--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>attribute_stats</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
--- /dev/null
+#Thu Jun 09 12:26:44 PDT 2011
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullReference=error
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
--- /dev/null
+Attribute Statistics
+---------------------
+
+This program gathers statistics about attribute usage in layout
+files. This is how the "topAttrs" attributes listed in ADT's
+extra-view-metadata.xml file (which drives the common attributes
+listed in the top of the context menu) is determined by running this
+script on a body of sample Android code, such as the AOSP repository.
+
+This program takes one or more directory paths, and then it searches
+all of them recursively for layout files that are not in folders
+containing the string "test", and computes and prints frequency
+statistics.
--- /dev/null
+/*
+ * 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.
+ */
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+/**
+ * Gathers statistics about attribute usage in layout files. This is how the "topAttrs"
+ * attributes listed in ADT's extra-view-metadata.xml (which drives the common attributes
+ * listed in the top of the context menu) is determined by running this script on a body
+ * of sample layout code.
+ * <p>
+ * This program takes one or more directory paths, and then it searches all of them recursively
+ * for layout files that are not in folders containing the string "test", and computes and
+ * prints frequency statistics.
+ */
+public class Analyzer {
+ /** Number of attributes to print for each view */
+ public static final int ATTRIBUTE_COUNT = 6;
+ /** Separate out any attributes that constitute less than N percent of the total */
+ public static final int THRESHOLD = 10; // percent
+
+ private List<File> mDirectories;
+ private File mCurrentFile;
+
+ /** Map from view id to map from attribute to frequency count */
+ private Map<String, Map<String, Usage>> mFrequencies =
+ new HashMap<String, Map<String, Usage>>(100);
+
+ private Map<String, Map<String, Usage>> mLayoutAttributeFrequencies =
+ new HashMap<String, Map<String, Usage>>(100);
+
+ private Map<String, String> mTopAttributes = new HashMap<String, String>(100);
+ private Map<String, String> mTopLayoutAttributes = new HashMap<String, String>(100);
+
+ private int mFileVisitCount;
+ private int mLayoutFileCount;
+ private File mXmlMetadataFile;
+
+ private Analyzer(List<File> directories, File xmlMetadataFile) {
+ mDirectories = directories;
+ mXmlMetadataFile = xmlMetadataFile;
+ }
+
+ public static void main(String[] args) {
+ if (args.length < 1) {
+ System.err.println("Usage: " + Analyzer.class.getSimpleName()
+ + " <directory1> [directory2 [directory3 ...]]\n");
+ System.err.println("Recursively scans for layouts in the given directory and");
+ System.err.println("computes statistics about attribute frequencies.");
+ System.exit(-1);
+ }
+
+ File metadataFile = null;
+ List<File> directories = new ArrayList<File>();
+ for (int i = 0, n = args.length; i < n; i++) {
+ String arg = args[i];
+
+ // The -metadata flag takes a pointer to an ADT extra-view-metadata.xml file
+ // and attempts to insert topAttrs attributes into it (and saves it as same
+ // file +.mod as an extension). This isn't listed on the usage flag because
+ // it's pretty brittle and requires some manual fixups to the file afterwards.
+ if (arg.equals("-metadata")) {
+ i++;
+ File file = new File(args[i]);
+ if (!file.exists()) {
+ System.err.println(file.getName() + " does not exist");
+ System.exit(-5);
+ }
+ if (!file.isFile() || !file.getName().endsWith(".xml")) {
+ System.err.println(file.getName() + " must be an XML file");
+ System.exit(-4);
+ }
+ metadataFile = file;
+ continue;
+ }
+ File directory = new File(arg);
+ if (!directory.exists()) {
+ System.err.println(directory.getName() + " does not exist");
+ System.exit(-2);
+ }
+
+ if (!directory.isDirectory()) {
+ System.err.println(directory.getName() + " is not a directory");
+ System.exit(-3);
+ }
+
+ directories.add(directory);
+ }
+
+ new Analyzer(directories, metadataFile).analyze();
+ }
+
+ private void analyze() {
+ for (File directory : mDirectories) {
+ scanDirectory(directory);
+ }
+ printStatistics();
+
+ if (mXmlMetadataFile != null) {
+ printMergedMetadata();
+ }
+ }
+
+ private void scanDirectory(File directory) {
+ File[] files = directory.listFiles();
+ if (files == null) {
+ return;
+ }
+
+ for (File file : files) {
+ mFileVisitCount++;
+ if (mFileVisitCount % 50000 == 0) {
+ System.out.println("Analyzed " + mFileVisitCount + " files...");
+ }
+
+ if (file.isFile()) {
+ scanFile(file);
+ } else if (file.isDirectory()) {
+ // Skip stuff related to tests
+ if (file.getName().contains("test")) {
+ continue;
+ }
+
+ // Recurse over subdirectories
+ scanDirectory(file);
+ }
+ }
+ }
+
+ private void scanFile(File file) {
+ if (file.getName().endsWith(".xml")) {
+ File parent = file.getParentFile();
+ if (parent.getName().startsWith("layout")) {
+ analyzeLayout(file);
+ }
+ }
+
+ }
+
+ private void analyzeLayout(File file) {
+ mCurrentFile = file;
+ mLayoutFileCount++;
+ Document document = null;
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ InputSource is = new InputSource(new StringReader(readFile(file)));
+ try {
+ factory.setNamespaceAware(true);
+ factory.setValidating(false);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ document = builder.parse(is);
+
+ analyzeDocument(document);
+
+ } catch (ParserConfigurationException e) {
+ // pass -- ignore files we can't parse
+ } catch (SAXException e) {
+ // pass -- ignore files we can't parse
+ } catch (IOException e) {
+ // pass -- ignore files we can't parse
+ }
+ }
+
+
+ private void analyzeDocument(Document document) {
+ analyzeElement(document.getDocumentElement());
+ }
+
+ private void analyzeElement(Element element) {
+ if (element.getTagName().equals("item")) {
+ // Resource files shouldn't be in the layout/ folder but I came across
+ // some cases
+ System.out.println("Warning: found <item> tag in a layout file in "
+ + mCurrentFile.getPath());
+ return;
+ }
+
+ countAttributes(element);
+ countLayoutAttributes(element);
+
+ // Recurse over children
+ NodeList childNodes = element.getChildNodes();
+ for (int i = 0, n = childNodes.getLength(); i < n; i++) {
+ Node child = childNodes.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ analyzeElement((Element) child);
+ }
+ }
+ }
+
+ private void countAttributes(Element element) {
+ String tag = element.getTagName();
+ Map<String, Usage> attributeMap = mFrequencies.get(tag);
+ if (attributeMap == null) {
+ attributeMap = new HashMap<String, Usage>(70);
+ mFrequencies.put(tag, attributeMap);
+ }
+
+ NamedNodeMap attributes = element.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Node attribute = attributes.item(i);
+ String name = attribute.getNodeName();
+
+ if (name.startsWith("android:layout_")) {
+ // Skip layout attributes; they are a function of the parent layout that this
+ // view is embedded within, not the view itself.
+ // TODO: Consider whether we should incorporate this info or make statistics
+ // about that as well?
+ continue;
+ }
+
+ if (name.equals("android:id")) {
+ // Skip ids: they are (mostly) unrelated to the view type and the tool
+ // already offers id editing prominently
+ continue;
+ }
+
+ if (name.startsWith("xmlns:")) {
+ // Unrelated to frequency counts
+ continue;
+ }
+
+ Usage usage = attributeMap.get(name);
+ if (usage == null) {
+ usage = new Usage(name);
+ } else {
+ usage.incrementCount();
+ }
+ attributeMap.put(name, usage);
+ }
+ }
+
+ private void countLayoutAttributes(Element element) {
+ String parentTag = element.getParentNode().getNodeName();
+ Map<String, Usage> attributeMap = mLayoutAttributeFrequencies.get(parentTag);
+ if (attributeMap == null) {
+ attributeMap = new HashMap<String, Usage>(70);
+ mLayoutAttributeFrequencies.put(parentTag, attributeMap);
+ }
+
+ NamedNodeMap attributes = element.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Node attribute = attributes.item(i);
+ String name = attribute.getNodeName();
+
+ if (!name.startsWith("android:layout_")) {
+ continue;
+ }
+
+ // Skip layout_width and layout_height; they are mandatory in all but GridLayout so not
+ // very interesting
+ if (name.equals("android:layout_width") || name.equals("android:layout_height")) {
+ continue;
+ }
+
+ Usage usage = attributeMap.get(name);
+ if (usage == null) {
+ usage = new Usage(name);
+ } else {
+ usage.incrementCount();
+ }
+ attributeMap.put(name, usage);
+ }
+ }
+
+ // Copied from AdtUtils
+ private static String readFile(File file) {
+ try {
+ return readFile(new FileReader(file));
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ private static String readFile(Reader inputStream) {
+ BufferedReader reader = null;
+ try {
+ reader = new BufferedReader(inputStream);
+ StringBuilder sb = new StringBuilder(2000);
+ while (true) {
+ int c = reader.read();
+ if (c == -1) {
+ return sb.toString();
+ } else {
+ sb.append((char)c);
+ }
+ }
+ } catch (IOException e) {
+ // pass -- ignore files we can't read
+ } finally {
+ try {
+ if (reader != null) {
+ reader.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return null;
+ }
+
+ private void printStatistics() {
+ System.out.println("Analyzed " + mLayoutFileCount
+ + " layouts (in a directory trees containing " + mFileVisitCount + " files)");
+ System.out.println("Top " + ATTRIBUTE_COUNT
+ + " for each view (excluding layout_ attributes) :");
+ System.out.println("\n");
+ System.out.println(" Rank Count Share Attribute");
+ System.out.println("=========================================================");
+ List<String> views = new ArrayList<String>(mFrequencies.keySet());
+ Collections.sort(views);
+ for (String view : views) {
+ String top = processUageMap(view, mFrequencies.get(view));
+ if (top != null) {
+ mTopAttributes.put(view, top);
+ }
+ }
+
+ System.out.println("\n\n\nTop " + ATTRIBUTE_COUNT + " layout attributes (excluding "
+ + "mandatory layout_width and layout_height):");
+ System.out.println("\n");
+ System.out.println(" Rank Count Share Attribute");
+ System.out.println("=========================================================");
+ views = new ArrayList<String>(mLayoutAttributeFrequencies.keySet());
+ Collections.sort(views);
+ for (String view : views) {
+ String top = processUageMap(view, mLayoutAttributeFrequencies.get(view));
+ if (top != null) {
+ mTopLayoutAttributes.put(view, top);
+ }
+ }
+ }
+
+ private static String processUageMap(String view, Map<String, Usage> map) {
+ if (map == null) {
+ return null;
+ }
+
+ if (view.indexOf('.') != -1 && !view.startsWith("android.")) {
+ // Skip custom views
+ return null;
+ }
+
+ List<Usage> values = new ArrayList<Usage>(map.values());
+ if (values.size() == 0) {
+ return null;
+ }
+
+ Collections.sort(values);
+ int totalCount = 0;
+ for (Usage usage : values) {
+ totalCount += usage.count;
+ }
+
+ System.out.println("\n<" + view + ">:");
+ if (view.equals("#document")) {
+ System.out.println("(Set on root tag, probably intended for included context)");
+ }
+
+ int place = 1;
+ int count = 0;
+ int prevCount = -1;
+ float prevPercentage = 0f;
+ StringBuilder sb = new StringBuilder();
+ for (Usage usage : values) {
+ if (count++ >= ATTRIBUTE_COUNT && usage.count < prevCount) {
+ break;
+ }
+
+ float percentage = 100 * usage.count/(float)totalCount;
+ if (percentage < THRESHOLD && prevPercentage >= THRESHOLD) {
+ System.out.println(" -----Less than 10%-------------------------------------");
+ }
+ System.out.printf(" %1d. %5d %5.1f%% %s\n", place, usage.count,
+ percentage, usage.attribute);
+
+ prevPercentage = percentage;
+ if (prevCount != usage.count) {
+ prevCount = usage.count;
+ place++;
+ }
+
+ if (percentage >= THRESHOLD /*&& usage.count > 1*/) { // 1:Ignore when not enough data?
+ if (sb.length() > 0) {
+ sb.append(',');
+ }
+ String name = usage.attribute;
+ if (name.startsWith("android:")) {
+ name = name.substring("android:".length());
+ }
+ sb.append(name);
+ }
+ }
+
+ return sb.length() > 0 ? sb.toString() : null;
+ }
+
+ private void printMergedMetadata() {
+ assert mXmlMetadataFile != null;
+ String metadata = readFile(mXmlMetadataFile);
+ if (metadata == null || metadata.length() == 0) {
+ System.err.println("Invalid metadata file");
+ System.exit(-6);
+ }
+
+ System.err.flush();
+ System.out.println("\n\nUpdating layout metadata file...");
+ System.out.flush();
+
+ StringBuilder sb = new StringBuilder((int) (2 * mXmlMetadataFile.length()));
+ String[] lines = metadata.split("\n");
+ for (int i = 0; i < lines.length; i++) {
+ String line = lines[i];
+ sb.append(line).append('\n');
+ int classIndex = line.indexOf("class=\"");
+ if (classIndex != -1) {
+ int start = classIndex + "class=\"".length();
+ int end = line.indexOf('"', start + 1);
+ if (end != -1) {
+ String view = line.substring(start, end);
+ if (view.startsWith("android.widget.")) {
+ view = view.substring("android.widget.".length());
+ } else if (view.startsWith("android.view.")) {
+ view = view.substring("android.view.".length());
+ } else if (view.startsWith("android.webkit.")) {
+ view = view.substring("android.webkit.".length());
+ }
+ String top = mTopAttributes.get(view);
+ if (top == null) {
+ System.err.println("Warning: No frequency data for view " + view);
+ } else {
+ sb.append(line.substring(0, classIndex)); // Indentation
+
+ sb.append("topAttrs=\"");
+ sb.append(top);
+ sb.append("\"\n");
+ }
+
+ top = mTopLayoutAttributes.get(view);
+ if (top != null) {
+ // It's a layout attribute
+ sb.append(line.substring(0, classIndex)); // Indentation
+
+ sb.append("topLayoutAttrs=\"");
+ sb.append(top);
+ sb.append("\"\n");
+ }
+ }
+ }
+ }
+
+ System.out.println("\nTop attributes:");
+ System.out.println("--------------------------");
+ List<String> views = new ArrayList<String>(mTopAttributes.keySet());
+ Collections.sort(views);
+ for (String view : views) {
+ String top = mTopAttributes.get(view);
+ System.out.println(view + ": " + top);
+ }
+
+ System.out.println("\nTop layout attributes:");
+ System.out.println("--------------------------");
+ views = new ArrayList<String>(mTopLayoutAttributes.keySet());
+ Collections.sort(views);
+ for (String view : views) {
+ String top = mTopLayoutAttributes.get(view);
+ System.out.println(view + ": " + top);
+ }
+
+ System.out.println("\nModified XML metadata file:\n");
+ String newContent = sb.toString();
+ File output = new File(mXmlMetadataFile.getParentFile(), mXmlMetadataFile.getName() + ".mod");
+ if (output.exists()) {
+ output.delete();
+ }
+ try {
+ BufferedWriter writer = new BufferedWriter(new FileWriter(output));
+ writer.write(newContent);
+ writer.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ System.out.println("Done - wrote " + output.getPath());
+ }
+
+ private static class Usage implements Comparable<Usage> {
+ public String attribute;
+ public int count;
+
+
+ public Usage(String attribute) {
+ super();
+ this.attribute = attribute;
+
+ count = 1;
+ }
+
+ public void incrementCount() {
+ count++;
+ }
+
+ public int compareTo(Usage o) {
+ // Sort by decreasing frequency, then sort alphabetically
+ int frequencyDelta = o.count - count;
+ if (frequencyDelta != 0) {
+ return frequencyDelta;
+ } else {
+ return attribute.compareTo(o.attribute);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return attribute + ": " + count;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((attribute == null) ? 0 : attribute.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Usage other = (Usage) obj;
+ if (attribute == null) {
+ if (other.attribute != null)
+ return false;
+ } else if (!attribute.equals(other.attribute))
+ return false;
+ return true;
+ }
+ }
+}
// Generate list of possible gravity value constants
assert IAttributeInfo.Format.FLAG.in(info.getFormats());
for (String name : info.getFlagValues()) {
- titles.add(prettyName(name));
+ titles.add(getAttributeDisplayName(name));
ids.add(name);
}
}
package com.android.ide.common.layout;
import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_HINT;
import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_STYLE;
import static com.android.ide.common.layout.LayoutConstants.ATTR_TEXT;
+import static com.android.ide.common.layout.LayoutConstants.DOT_LAYOUT_PARAMS;
import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX;
import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
import static com.android.ide.common.layout.LayoutConstants.VALUE_FILL_PARENT;
import com.android.ide.common.api.IMenuCallback;
import com.android.ide.common.api.INode;
import com.android.ide.common.api.IValidator;
+import com.android.ide.common.api.IViewMetadata;
import com.android.ide.common.api.IViewRule;
import com.android.ide.common.api.InsertType;
import com.android.ide.common.api.Point;
import com.android.ide.common.api.RuleAction;
import com.android.ide.common.api.RuleAction.ActionProvider;
import com.android.ide.common.api.RuleAction.ChoiceProvider;
-import com.android.ide.common.api.RuleAction.Choices;
import com.android.ide.common.api.SegmentType;
+import com.android.resources.ResourceType;
import com.android.util.Pair;
import java.net.URL;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
* Common IViewRule processing to all view and layout classes.
*/
public class BaseViewRule implements IViewRule {
+ /** List of recently edited properties */
+ private static List<String> sRecent = new LinkedList<String>();
+
+ /** Maximum number of recent properties to track and list */
+ private final static int MAX_RECENT_COUNT = 12;
+
// Strings used as internal ids, group ids and prefixes for actions
private static final String FALSE_ID = "false"; //$NON-NLS-1$
private static final String TRUE_ID = "true"; //$NON-NLS-1$
private static final String PROP_PREFIX = "@prop@"; //$NON-NLS-1$
private static final String CLEAR_ID = "clear"; //$NON-NLS-1$
- private static final String PROPERTIES_ID = "properties"; //$NON-NLS-1$
- private static final String EDIT_TEXT_ID = "edittext"; //$NON-NLS-1$
- private static final String EDIT_ID_ID = "editid"; //$NON-NLS-1$
- private static final String WIDTH_ID = "layout_width"; //$NON-NLS-1$
- private static final String HEIGHT_ID = "layout_height"; //$NON-NLS-1$
private static final String ZCUSTOM = "zcustom"; //$NON-NLS-1$
protected IClientRulesEngine mRulesEngine;
final String actionId = isProp ?
fullActionId.substring(PROP_PREFIX.length()) : fullActionId;
- if (fullActionId.equals(WIDTH_ID)) {
+ if (fullActionId.equals(ATTR_LAYOUT_WIDTH)) {
final String newAttrValue = getValue(valueId, newWidth);
if (newAttrValue != null) {
for (INode node : selectedNodes) {
new PropertySettingNodeHandler(ANDROID_URI,
ATTR_LAYOUT_WIDTH, newAttrValue));
}
+ editedProperty(ATTR_LAYOUT_WIDTH);
}
return;
- } else if (fullActionId.equals(HEIGHT_ID)) {
+ } else if (fullActionId.equals(ATTR_LAYOUT_HEIGHT)) {
// Ask the user
final String newAttrValue = getValue(valueId, newHeight);
if (newAttrValue != null) {
new PropertySettingNodeHandler(ANDROID_URI,
ATTR_LAYOUT_HEIGHT, newAttrValue));
}
+ editedProperty(ATTR_LAYOUT_HEIGHT);
}
return;
- } else if (fullActionId.equals(EDIT_ID_ID)) {
+ } else if (fullActionId.equals(ATTR_ID)) {
// Ids must be set individually so open the id dialog for each
// selected node (though allow cancel to break the loop)
for (INode node : selectedNodes) {
}
node.editXml("Change ID", new PropertySettingNodeHandler(ANDROID_URI,
ATTR_ID, newId));
+ editedProperty(ATTR_ID);
} else if (newId == null) {
// Cancelled
break;
}
}
return;
- } else {
+ } else if (isProp) {
INode firstNode = selectedNodes.get(0);
- if (fullActionId.equals(EDIT_TEXT_ID)) {
- String oldText = selectedNodes.size() == 1
- ? firstNode.getStringAttr(ANDROID_URI, ATTR_TEXT)
- : ""; //$NON-NLS-1$
- oldText = ensureValidString(oldText);
- String newText = mRulesEngine.displayResourceInput("string", oldText); //$NON-NLS-1$
- if (newText != null) {
- for (INode node : selectedNodes) {
- node.editXml("Change Text",
- new PropertySettingNodeHandler(ANDROID_URI,
- ATTR_TEXT, newText.length() > 0 ? newText : null));
+ String key = getPropertyMapKey(selectedNode);
+ Map<String, Prop> props = mAttributesMap.get(key);
+ final Prop prop = (props != null) ? props.get(actionId) : null;
+
+ if (prop != null) {
+ editedProperty(actionId);
+
+ // For custom values (requiring an input dialog) input the
+ // value outside the undo-block.
+ // Input the value as a text, unless we know it's the "text" or
+ // "style" attributes (where we know we want to ask for specific
+ // resource types).
+ String uri = ANDROID_URI;
+ String v = null;
+ if (prop.isStringEdit()) {
+ boolean isStyle = actionId.equals(ATTR_STYLE);
+ boolean isText = actionId.equals(ATTR_TEXT);
+ boolean isHint = actionId.equals(ATTR_HINT);
+ if (isStyle || isText || isHint) {
+ String resourceTypeName = isStyle
+ ? ResourceType.STYLE.getName()
+ : ResourceType.STRING.getName();
+ String oldValue = selectedNodes.size() == 1
+ ? firstNode.getStringAttr(null, ATTR_STYLE)
+ : ""; //$NON-NLS-1$
+ oldValue = ensureValidString(oldValue);
+ v = mRulesEngine.displayResourceInput(resourceTypeName, oldValue);
+ if (isStyle) {
+ uri = null;
+ }
+ } else {
+ v = inputAttributeValue(firstNode, actionId);
}
}
- return;
- } else if (isProp) {
- String key = getPropertyMapKey(selectedNode);
- Map<String, Prop> props = mAttributesMap.get(key);
- final Prop prop = (props != null) ? props.get(actionId) : null;
-
- if (prop != null) {
- // For custom values (requiring an input dialog) input the
- // value outside the undo-block
- final String customValue = prop.isStringEdit()
- ? inputAttributeValue(firstNode, actionId) : null;
-
- for (INode n : selectedNodes) {
- if (prop.isToggle()) {
- // case of toggle
- String value = ""; //$NON-NLS-1$
- if (valueId.equals(TRUE_ID)) {
- value = newValue ? "true" : ""; //$NON-NLS-1$ //$NON-NLS-2$
- } else if (valueId.equals(FALSE_ID)) {
- value = newValue ? "false" : "";//$NON-NLS-1$ //$NON-NLS-2$
- }
- n.setAttribute(ANDROID_URI, actionId, value);
- } else if (prop.isFlag()) {
- // case of a flag
- String values = ""; //$NON-NLS-1$
- if (!valueId.equals(CLEAR_ID)) {
- values = n.getStringAttr(ANDROID_URI, actionId);
- Set<String> newValues = new HashSet<String>();
- if (values != null) {
- newValues.addAll(Arrays.asList(
- values.split("\\|"))); //$NON-NLS-1$
- }
- if (newValue) {
- newValues.add(valueId);
- } else {
- newValues.remove(valueId);
- }
- values = join('|', newValues);
- }
- n.setAttribute(ANDROID_URI, actionId, values);
- } else if (prop.isEnum()) {
- // case of an enum
- String value = ""; //$NON-NLS-1$
- if (!valueId.equals(CLEAR_ID)) {
- value = newValue ? valueId : ""; //$NON-NLS-1$
+ final String customValue = v;
+
+ for (INode n : selectedNodes) {
+ if (prop.isToggle()) {
+ // case of toggle
+ String value = ""; //$NON-NLS-1$
+ if (valueId.equals(TRUE_ID)) {
+ value = newValue ? "true" : ""; //$NON-NLS-1$ //$NON-NLS-2$
+ } else if (valueId.equals(FALSE_ID)) {
+ value = newValue ? "false" : "";//$NON-NLS-1$ //$NON-NLS-2$
+ }
+ n.setAttribute(uri, actionId, value);
+ } else if (prop.isFlag()) {
+ // case of a flag
+ String values = ""; //$NON-NLS-1$
+ if (!valueId.equals(CLEAR_ID)) {
+ values = n.getStringAttr(ANDROID_URI, actionId);
+ Set<String> newValues = new HashSet<String>();
+ if (values != null) {
+ newValues.addAll(Arrays.asList(
+ values.split("\\|"))); //$NON-NLS-1$
}
- n.setAttribute(ANDROID_URI, actionId, value);
- } else {
- assert prop.isStringEdit();
- // We've already received the value outside the undo block
- if (customValue != null) {
- n.setAttribute(ANDROID_URI, actionId, customValue);
+ if (newValue) {
+ newValues.add(valueId);
+ } else {
+ newValues.remove(valueId);
}
+ values = join('|', newValues);
+ }
+ n.setAttribute(uri, actionId, values);
+ } else if (prop.isEnum()) {
+ // case of an enum
+ String value = ""; //$NON-NLS-1$
+ if (!valueId.equals(CLEAR_ID)) {
+ value = newValue ? valueId : ""; //$NON-NLS-1$
+ }
+ n.setAttribute(uri, actionId, value);
+ } else {
+ assert prop.isStringEdit();
+ // We've already received the value outside the undo block
+ if (customValue != null) {
+ n.setAttribute(uri, actionId, customValue);
}
}
}
IAttributeInfo textAttribute = selectedNode.getAttributeInfo(ANDROID_URI, ATTR_TEXT);
if (textAttribute != null) {
- actions.add(RuleAction.createAction(EDIT_TEXT_ID, "Edit Text...", onChange,
+ actions.add(RuleAction.createAction(PROP_PREFIX + ATTR_TEXT, "Edit Text...", onChange,
null, 10, true));
}
- actions.add(RuleAction.createAction(EDIT_ID_ID, "Edit ID...", onChange, null, 20, true));
+ actions.add(RuleAction.createAction(ATTR_ID, "Edit ID...", onChange, null, 20, true));
+
+ addCommonPropertyActions(actions, selectedNode, onChange, 21);
// Create width choice submenu
+ actions.add(RuleAction.createSeparator(32));
List<Pair<String, String>> widthChoices = new ArrayList<Pair<String,String>>(4);
widthChoices.add(Pair.of(VALUE_WRAP_CONTENT, "Wrap Content"));
if (canMatchParent) {
}
widthChoices.add(Pair.of(ZCUSTOM, "Other..."));
actions.add(RuleAction.createChoices(
- WIDTH_ID, "Layout Width",
+ ATTR_LAYOUT_WIDTH, "Layout Width",
onChange,
null /* iconUrls */,
currentWidth,
- null, 30,
+ null, 35,
true, // supportsMultipleNodes
widthChoices));
}
heightChoices.add(Pair.of(ZCUSTOM, "Other..."));
actions.add(RuleAction.createChoices(
- HEIGHT_ID, "Layout Height",
+ ATTR_LAYOUT_HEIGHT, "Layout Height",
onChange,
null /* iconUrls */,
currentHeight,
heightChoices));
actions.add(RuleAction.createSeparator(45));
- RuleAction properties = RuleAction.createChoices(PROPERTIES_ID, "Properties",
+ RuleAction properties = RuleAction.createChoices("properties", "Other Properties", //$NON-NLS-1$
onChange /*callback*/, null /*icon*/, 50,
true /*supportsMultipleNodes*/, new ActionProvider() {
public List<RuleAction> getNestedActions(INode node) {
- List<RuleAction> propertyActions = createPropertyActions(node,
- getPropertyMapKey(node), onChange);
+ List<RuleAction> propertyActionTypes = new ArrayList<RuleAction>();
+ propertyActionTypes.add(RuleAction.createChoices(
+ "recent", "Recent", //$NON-NLS-1$
+ onChange /*callback*/, null /*icon*/, 10,
+ true /*supportsMultipleNodes*/, new ActionProvider() {
+ public List<RuleAction> getNestedActions(INode n) {
+ List<RuleAction> propertyActions = new ArrayList<RuleAction>();
+ addRecentPropertyActions(propertyActions, n, onChange);
+ return propertyActions;
+ }
+ }));
+
+ propertyActionTypes.add(RuleAction.createSeparator(20));
+
+ addInheritedProperties(propertyActionTypes, node, onChange, 30);
- return propertyActions;
+ propertyActionTypes.add(RuleAction.createSeparator(50));
+ propertyActionTypes.add(RuleAction.createChoices(
+ "layoutparams", "Layout Parameters", //$NON-NLS-1$
+ onChange /*callback*/, null /*icon*/, 60,
+ true /*supportsMultipleNodes*/, new ActionProvider() {
+ public List<RuleAction> getNestedActions(INode n) {
+ List<RuleAction> propertyActions = new ArrayList<RuleAction>();
+ addPropertyActions(propertyActions, n, onChange, null, true);
+ return propertyActions;
+ }
+ }));
+
+ propertyActionTypes.add(RuleAction.createSeparator(70));
+
+ propertyActionTypes.add(RuleAction.createChoices(
+ "allprops", "All By Name", //$NON-NLS-1$
+ onChange /*callback*/, null /*icon*/, 80,
+ true /*supportsMultipleNodes*/, new ActionProvider() {
+ public List<RuleAction> getNestedActions(INode n) {
+ List<RuleAction> propertyActions = new ArrayList<RuleAction>();
+ addPropertyActions(propertyActions, n, onChange, null, false);
+ return propertyActions;
+ }
+ }));
+
+ return propertyActionTypes;
}
});
}
/**
+ * Adds menu items for the inherited attributes, one pull-right menu for each super class
+ * that defines attributes.
+ *
+ * @param propertyActionTypes the actions list to add into
+ * @param node the node to apply the attributes to
+ * @param onChange the callback to use for setting attributes
+ * @param sortPriority the initial sort attribute for the first menu item
+ */
+ private void addInheritedProperties(List<RuleAction> propertyActionTypes, INode node,
+ final IMenuCallback onChange, int sortPriority) {
+ List<String> attributeSources = node.getAttributeSources();
+ for (final String definedBy : attributeSources) {
+ String sourceClass = definedBy;
+
+ // Strip package prefixes when necessary
+ int index = sourceClass.length();
+ if (sourceClass.endsWith(DOT_LAYOUT_PARAMS)) {
+ index = sourceClass.length() - DOT_LAYOUT_PARAMS.length() - 1;
+ }
+ int lastDot = sourceClass.lastIndexOf('.', index);
+ if (lastDot != -1) {
+ sourceClass = sourceClass.substring(lastDot + 1);
+ }
+
+ String label;
+ if (definedBy.equals(node.getFqcn())) {
+ label = String.format("Defined by %1$s", sourceClass);
+ } else {
+ label = String.format("Inherited from %1$s", sourceClass);
+ }
+
+ propertyActionTypes.add(RuleAction.createChoices("def_" + definedBy,
+ label,
+ onChange /*callback*/, null /*icon*/, sortPriority++,
+ true /*supportsMultipleNodes*/, new ActionProvider() {
+ public List<RuleAction> getNestedActions(INode n) {
+ List<RuleAction> propertyActions = new ArrayList<RuleAction>();
+ addPropertyActions(propertyActions, n, onChange, definedBy, false);
+ return propertyActions;
+ }
+ }));
+ }
+ }
+
+ /**
+ * Creates a list of properties that are commonly edited for views of the
+ * selected node's type
+ */
+ private void addCommonPropertyActions(List<RuleAction> actions, INode selectedNode,
+ IMenuCallback onChange, int sortPriority) {
+ Map<String, Prop> properties = getPropertyMetadata(selectedNode);
+ IViewMetadata metadata = mRulesEngine.getMetadata(selectedNode.getFqcn());
+ if (metadata != null) {
+ List<String> attributes = metadata.getTopAttributes();
+ if (attributes.size() > 0) {
+ for (String attribute : attributes) {
+ // Text and ID are handled manually in the menu construction code because
+ // we want to place them consistently and customize the action label
+ if (ATTR_TEXT.equals(attribute) || ATTR_ID.equals(attribute)) {
+ continue;
+ }
+
+ Prop property = properties.get(attribute);
+ if (property != null) {
+ String title = property.getTitle();
+ if (title.endsWith("...")) {
+ title = String.format("Edit %1$s", property.getTitle());
+ }
+ actions.add(createPropertyAction(property, attribute, title,
+ selectedNode, onChange, sortPriority));
+ sortPriority++;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Record that the given property was just edited; adds it to the front of
+ * the recently edited property list
+ *
+ * @param property the name of the property
+ */
+ static void editedProperty(String property) {
+ if (sRecent.contains(property)) {
+ sRecent.remove(property);
+ } else if (sRecent.size() > MAX_RECENT_COUNT) {
+ sRecent.remove(sRecent.size() - 1);
+ }
+ sRecent.add(0, property);
+ }
+
+ /**
+ * Creates a list of recently modified properties that apply to the given selected node
+ */
+ private void addRecentPropertyActions(List<RuleAction> actions, INode selectedNode,
+ IMenuCallback onChange) {
+ int sortPriority = 10;
+ Map<String, Prop> properties = getPropertyMetadata(selectedNode);
+ for (String attribute : sRecent) {
+ Prop property = properties.get(attribute);
+ if (property != null) {
+ actions.add(createPropertyAction(property, attribute, property.getTitle(),
+ selectedNode, onChange, sortPriority));
+ sortPriority += 10;
+ }
+ }
+ }
+
+ /**
* Creates a list of nested actions representing the property-setting
* actions for the given selected node
*/
- private List<RuleAction> createPropertyActions(final INode selectedNode, final String key,
- final IMenuCallback onChange) {
- List<RuleAction> propertyActions = new ArrayList<RuleAction>();
+ private void addPropertyActions(List<RuleAction> actions, INode selectedNode,
+ IMenuCallback onChange, String definedBy, boolean layoutParamsOnly) {
+
+ Map<String, Prop> properties = getPropertyMetadata(selectedNode);
+
+ int sortPriority = 10;
+ for (Map.Entry<String, Prop> entry : properties.entrySet()) {
+ String id = entry.getKey();
+ Prop property = entry.getValue();
+ if (layoutParamsOnly) {
+ // If we have definedBy information, that is most accurate; all layout
+ // params will be defined by a class whose name ends with
+ // .LayoutParams:
+ if (definedBy != null) {
+ if (!definedBy.endsWith(DOT_LAYOUT_PARAMS)) {
+ continue;
+ }
+ } else if (!id.startsWith(ATTR_LAYOUT_PREFIX)) {
+ continue;
+ }
+ }
+ if (definedBy != null && !definedBy.equals(property.getDefinedBy())) {
+ continue;
+ }
+ actions.add(createPropertyAction(property, id, property.getTitle(),
+ selectedNode, onChange, sortPriority));
+ sortPriority += 10;
+ }
+
+ // The properties are coming out of map key order which isn't right, so sort
+ // alphabetically instead
+ Collections.sort(actions, new Comparator<RuleAction>() {
+ public int compare(RuleAction action1, RuleAction action2) {
+ return action1.getTitle().compareTo(action2.getTitle());
+ }
+ });
+ }
+
+ private RuleAction createPropertyAction(Prop p, String id, String title, INode selectedNode,
+ IMenuCallback onChange, int sortPriority) {
+ if (p.isToggle()) {
+ // Toggles are handled as a multiple-choice between true, false
+ // and nothing (clear)
+ String value = selectedNode.getStringAttr(ANDROID_URI, id);
+ if (value != null)
+ value = value.toLowerCase();
+ if ("true".equals(value)) { //$NON-NLS-1$
+ value = TRUE_ID;
+ } else if ("false".equals(value)) { //$NON-NLS-1$
+ value = FALSE_ID;
+ } else {
+ value = CLEAR_ID;
+ }
+ return RuleAction.createChoices(PROP_PREFIX + id, title,
+ onChange, BOOLEAN_CHOICE_PROVIDER,
+ value,
+ null, sortPriority,
+ true);
+ } else if (p.getChoices() != null) {
+ // Enum or flags. Their possible values are the multiple-choice
+ // items, with an extra "clear" option to remove everything.
+ String current = selectedNode.getStringAttr(ANDROID_URI, id);
+ if (current == null || current.length() == 0) {
+ current = CLEAR_ID;
+ }
+ return RuleAction.createChoices(PROP_PREFIX + id, title,
+ onChange, new EnumPropertyChoiceProvider(p),
+ current,
+ null, sortPriority,
+ true);
+ } else {
+ return RuleAction.createAction(
+ PROP_PREFIX + id,
+ title,
+ onChange,
+ null, sortPriority,
+ true);
+ }
+ }
+ private Map<String, Prop> getPropertyMetadata(final INode selectedNode) {
+ String key = getPropertyMapKey(selectedNode);
Map<String, Prop> props = mAttributesMap.get(key);
if (props == null) {
// Prepare the property map
continue;
}
- String title = prettyName(id);
+ String title = getAttributeDisplayName(id);
+ String definedBy = attrInfo != null ? attrInfo.getDefinedBy() : null;
if (IAttributeInfo.Format.BOOLEAN.in(formats)) {
- props.put(id, new Prop(title, true));
+ props.put(id, new Prop(title, true, definedBy));
} else if (IAttributeInfo.Format.ENUM.in(formats)) {
// Convert each enum into a map id=>title
Map<String, String> values = new HashMap<String, String>();
if (attrInfo != null) {
for (String e : attrInfo.getEnumValues()) {
- values.put(e, prettyName(e));
+ values.put(e, getAttributeDisplayName(e));
}
}
- props.put(id, new Prop(title, false, false, values));
+ props.put(id, new Prop(title, false, false, values, definedBy));
} else if (IAttributeInfo.Format.FLAG.in(formats)) {
// Convert each flag into a map id=>title
Map<String, String> values = new HashMap<String, String>();
if (attrInfo != null) {
for (String e : attrInfo.getFlagValues()) {
- values.put(e, prettyName(e));
+ values.put(e, getAttributeDisplayName(e));
}
}
- props.put(id, new Prop(title, false, true, values));
+ props.put(id, new Prop(title, false, true, values, definedBy));
} else {
- props.put(id, new Prop(title + "...", false));
+ props.put(id, new Prop(title + "...", false, definedBy));
}
}
mAttributesMap.put(key, props);
}
-
- int nextPriority = 10;
- for (Map.Entry<String, Prop> entry : props.entrySet()) {
- String id = entry.getKey();
- Prop p = entry.getValue();
- if (p.isToggle()) {
- // Toggles are handled as a multiple-choice between true, false
- // and nothing (clear)
- String value = selectedNode.getStringAttr(ANDROID_URI, id);
- if (value != null)
- value = value.toLowerCase();
- if ("true".equals(value)) { //$NON-NLS-1$
- value = TRUE_ID;
- } else if ("false".equals(value)) { //$NON-NLS-1$
- value = FALSE_ID;
- } else {
- value = CLEAR_ID;
- }
- Choices action = RuleAction.createChoices(PROP_PREFIX + id, p.getTitle(),
- onChange, BOOLEAN_CHOICE_PROVIDER,
- value,
- null, nextPriority++,
- true);
- propertyActions.add(action);
- } else if (p.getChoices() != null) {
- // Enum or flags. Their possible values are the multiple-choice
- // items, with an extra "clear" option to remove everything.
- String current = selectedNode.getStringAttr(ANDROID_URI, id);
- if (current == null || current.length() == 0) {
- current = CLEAR_ID;
- }
- Choices action = RuleAction.createChoices(PROP_PREFIX + id, p.getTitle(),
- onChange, new EnumPropertyChoiceProvider(p),
- current,
- null, nextPriority++,
- true);
- propertyActions.add(action);
- } else {
- RuleAction action = RuleAction.createAction(
- PROP_PREFIX + id,
- p.getTitle(),
- onChange,
- null, nextPriority++,
- true);
- propertyActions.add(action);
- }
- }
-
- // The properties are coming out of map key order which isn't right
- Collections.sort(propertyActions, new Comparator<RuleAction>() {
- public int compare(RuleAction action1, RuleAction action2) {
- return action1.getTitle().compareTo(action2.getTitle());
- }
- });
- return propertyActions;
+ return props;
}
/**
return map;
}
- public static String prettyName(String name) {
+ /**
+ * Produces a display name for an attribute, usually capitalizing the attribute name
+ * and splitting up underscores into new words
+ *
+ * @param name the attribute name to convert
+ * @return a display name for the attribute name
+ */
+ public static String getAttributeDisplayName(String name) {
if (name != null && name.length() > 0) {
- name = Character.toUpperCase(name.charAt(0)) + name.substring(1).replace('_', ' ');
+ StringBuilder sb = new StringBuilder();
+ boolean capitalizeNext = true;
+ for (int i = 0, n = name.length(); i < n; i++) {
+ char c = name.charAt(i);
+ if (capitalizeNext) {
+ c = Character.toUpperCase(c);
+ }
+ capitalizeNext = false;
+ if (c == '_') {
+ c = ' ';
+ capitalizeNext = true;
+ }
+ sb.append(c);
+ }
+
+ return sb.toString();
}
return name;
private final boolean mFlag;
private final String mTitle;
private final Map<String, String> mChoices;
+ private String mDefinedBy;
+
+ public Prop(String title, boolean isToggle, boolean isFlag, Map<String, String> choices,
+ String definedBy) {
+ mTitle = title;
+ mToggle = isToggle;
+ mFlag = isFlag;
+ mChoices = choices;
+ mDefinedBy = definedBy;
+ }
- public Prop(String title, boolean isToggle, boolean isFlag, Map<String, String> choices) {
- this.mTitle = title;
- this.mToggle = isToggle;
- this.mFlag = isFlag;
- this.mChoices = choices;
+ public String getDefinedBy() {
+ return mDefinedBy;
}
- public Prop(String title, boolean isToggle) {
- this(title, isToggle, false, null);
+ public Prop(String title, boolean isToggle, String definedBy) {
+ this(title, isToggle, false, null, definedBy);
}
private boolean isToggle() {
public void onRemovingChildren(List<INode> deleted, INode parent) {
}
+ /**
+ * Strips the {@code @+id} or {@code @id} prefix off of the given id
+ *
+ * @param id attribute to be stripped
+ * @return the id name without the {@code @+id} or {@code @id} prefix
+ */
public static String stripIdPrefix(String id) {
if (id == null) {
return ""; //$NON-NLS-1$
actions.add(RuleAction.createAction("_setfocus", label, onChange, //$NON-NLS-1$
null, 5, false /*supportsMultipleNodes*/));
+ actions.add(RuleAction.createSeparator(7));
}
/** Returns true if the given node currently has focus */
/** The android.webkit. package prefix */
public static final String ANDROID_WEBKIT_PKG = ANDROID_PKG_PREFIX + "webkit."; //$NON-NLS-1$
+ /** The LayoutParams inner-class name suffix, .LayoutParams */
+ public static final String DOT_LAYOUT_PARAMS = ".LayoutParams"; //$NON-NLS-1$
+
/** The fully qualified class name of an EditText view */
public static final String FQCN_EDIT_TEXT = "android.widget.EditText"; //$NON-NLS-1$
import com.android.ide.common.api.IViewMetadata.FillPreference;
import com.android.ide.common.api.IViewRule;
import com.android.ide.common.api.InsertType;
-import com.android.ide.common.api.RuleAction;
-import com.android.ide.common.api.RuleAction.Choices;
import com.android.ide.common.api.Point;
import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.RuleAction;
+import com.android.ide.common.api.RuleAction.Choices;
import com.android.ide.common.api.SegmentType;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.sdklib.SdkConstants;
-import com.android.util.Pair;
import java.net.URL;
import java.util.ArrayList;
LinearLayoutRule.class.getResource("allweight.png"); //$NON-NLS-1$
/**
- * Add an explicit Orientation toggle to the context menu.
- */
- @Override
- public void addContextMenuActions(List<RuleAction> actions, final INode selectedNode) {
- super.addContextMenuActions(actions, selectedNode);
- if (supportsOrientation()) {
- String current = getCurrentOrientation(selectedNode);
- IMenuCallback onChange = new PropertyCallback(
- null, // use passed in nodes instead to support multiple nodes
- "Change LinearLayout Orientation",
- ANDROID_URI, ATTR_ORIENTATION);
- List<Pair<String, String>> alternatives = new ArrayList<Pair<String,String>>(2);
- alternatives.add(Pair.of("horizontal", "Horizontal")); //$NON-NLS-1$
- alternatives.add(Pair.of("vertical", "Vertical")); //$NON-NLS-1$
- RuleAction action = RuleAction.createChoices(
- ACTION_ORIENTATION, "Orientation", //$NON-NLS-1$
- onChange,
- null /* iconUrls */,
- current,
- null /* icon */, 5, true,
- alternatives);
-
- actions.add(action);
- }
- }
-
- /**
* Returns the current orientation, regardless of whether it has been defined in XML
*
* @param node The LinearLayout to look up the orientation for
private String mJavaDoc;
/** Documentation for deprecated attributes. Null if not deprecated. */
private String mDeprecatedDoc;
+ /** The source class defining this attribute */
+ private String mDefinedBy;
/**
* @param name The XML Name of the attribute
public void setDeprecatedDoc(String deprecatedDoc) {
mDeprecatedDoc = deprecatedDoc;
}
+
+ /**
+ * Sets the name of the class (fully qualified class name) which defined
+ * this attribute
+ *
+ * @param definedBy the name of the class (fully qualified class name) which
+ * defined this attribute
+ */
+ public void setDefinedBy(String definedBy) {
+ mDefinedBy = definedBy;
+ }
+
+ /**
+ * Returns the name of the class (fully qualified class name) which defined
+ * this attribute
+ *
+ * @return the name of the class (fully qualified class name) which defined
+ * this attribute
+ */
+ public String getDefinedBy() {
+ return mDefinedBy;
+ }
}
package com.android.ide.common.resources.platform;
+import static com.android.ide.common.layout.LayoutConstants.DOT_LAYOUT_PARAMS;
+
import com.android.ide.common.api.IAttributeInfo.Format;
import com.android.ide.common.log.ILogger;
import com.android.ide.common.resources.platform.ViewClassInfo.LayoutParamsInfo;
String xmlName = info.getShortClassName();
DeclareStyleableInfo style = mStyleMap.get(xmlName);
if (style != null) {
- info.setAttributes(style.getAttributes());
+ String definedBy = info.getFullClassName();
+ AttributeInfo[] attributes = style.getAttributes();
+ for (AttributeInfo attribute : attributes) {
+ if (attribute.getDefinedBy() == null) {
+ attribute.setDefinedBy(definedBy);
+ }
+ }
+ info.setAttributes(attributes);
info.setJavaDoc(style.getJavaDoc());
}
}
public void loadLayoutParamsAttributes(LayoutParamsInfo info) {
if (getDocument() != null) {
// Transforms "LinearLayout" and "LayoutParams" into "LinearLayout_Layout".
+ ViewClassInfo viewLayoutClass = info.getViewLayoutClass();
String xmlName = String.format("%1$s_%2$s", //$NON-NLS-1$
- info.getViewLayoutClass().getShortClassName(),
+ viewLayoutClass.getShortClassName(),
info.getShortClassName());
xmlName = xmlName.replaceFirst("Params$", ""); //$NON-NLS-1$ //$NON-NLS-2$
DeclareStyleableInfo style = mStyleMap.get(xmlName);
if (style != null) {
- info.setAttributes(style.getAttributes());
+ // For defined by, use the actual class name, e.g.
+ // android.widget.LinearLayout.LayoutParams
+ String definedBy = viewLayoutClass.getFullClassName() + DOT_LAYOUT_PARAMS;
+ AttributeInfo[] attributes = style.getAttributes();
+ for (AttributeInfo attribute : attributes) {
+ if (attribute.getDefinedBy() == null) {
+ attribute.setDefinedBy(definedBy);
+ }
+ }
+ info.setAttributes(attributes);
}
}
}
*/
public class DeclareStyleableInfo {
/** The style name, never null. */
- private String mStyleName;
+ private final String mStyleName;
/** Attributes for this view or view group. Can be empty but never null. */
- private AttributeInfo[] mAttributes;
+ private final AttributeInfo[] mAttributes;
/** Short javadoc. Can be null. */
private String mJavaDoc;
- /** Optional name of the parents stylable. Can be null. */
+ /** Optional name of the parents styleable. Can be null. */
private String[] mParents;
/**
}
}
-
/** Returns style name */
public String getStyleName() {
return mStyleName;
return mAttributes;
}
- /** Sets the list of attributes for this View or ViewGroup. */
- public void setAttributes(AttributeInfo[] attributes) {
- mAttributes = attributes;
- }
-
/** Returns a short javadoc */
public String getJavaDoc() {
return mJavaDoc;
return mAttributes;
}
- /* Sets the list of allowed attributes. */
+ /** Sets the list of allowed attributes. */
public void setAttributes(AttributeDescriptor[] attributes) {
mAttributes = attributes;
for (AttributeDescriptor attribute : attributes) {
styleInfo,
false, //required
null); // overrides
+ styleInfo.setDefinedBy(SdkConstants.CLASS_VIEW);
// Process all View attributes
DescriptorsUtils.appendAttributes(attributes,
null, // requiredAttributes
null /* overrides */);
+ List<String> attributeSources = new ArrayList<String>();
+ if (info.getAttributes() != null && info.getAttributes().length > 0) {
+ attributeSources.add(fqcn);
+ }
+
for (ViewClassInfo link = info.getSuperClass();
link != null;
link = link.getSuperClass()) {
AttributeInfo[] attrList = link.getAttributes();
if (attrList.length > 0) {
+ attributeSources.add(link.getFullClassName());
attributes.add(new SeparatorAttributeDescriptor(
String.format("Attributes from %1$s", link.getShortClassName())));
DescriptorsUtils.appendAttributes(attributes,
continue;
}
if (needSeparator) {
+ ViewClassInfo viewLayoutClass = layoutParams.getViewLayoutClass();
String title;
+ String shortClassName = viewLayoutClass.getShortClassName();
if (layoutParams.getShortClassName().equals(
SdkConstants.CLASS_NAME_LAYOUTPARAMS)) {
title = String.format("Layout Attributes from %1$s",
- layoutParams.getViewLayoutClass().getShortClassName());
+ shortClassName);
} else {
title = String.format("Layout Attributes from %1$s (%2$s)",
- layoutParams.getViewLayoutClass().getShortClassName(),
+ shortClassName,
layoutParams.getShortClassName());
}
layoutAttributes.add(new SeparatorAttributeDescriptor(title));
layoutAttributes.toArray(new AttributeDescriptor[layoutAttributes.size()]),
null, // children
false /* mandatory */);
+ desc.setAttributeSources(Collections.unmodifiableList(attributeSources));
infoDescMap.put(info, desc);
return desc;
}
package com.android.ide.eclipse.adt.internal.editors.layout.descriptors;
-import static com.android.ide.common.layout.LayoutConstants.ANDROID_WIDGET_PREFIX;
import static com.android.ide.common.layout.LayoutConstants.ANDROID_VIEW_PKG;
import static com.android.ide.common.layout.LayoutConstants.ANDROID_WEBKIT_PKG;
+import static com.android.ide.common.layout.LayoutConstants.ANDROID_WIDGET_PREFIX;
+import com.android.ide.common.resources.platform.AttributeInfo;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.IconFactory;
import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
import org.eclipse.swt.graphics.Image;
+import java.util.Collections;
+import java.util.List;
+
/**
* {@link ViewElementDescriptor} describes the properties expected for a given XML element node
* representing a class in an XML Layout file.
/** The super-class descriptor. Can be null. */
private ViewElementDescriptor mSuperClassDesc;
+ /** List of attribute sources, classes that contribute attributes to {@link #mAttributes} */
+ private List<String> mAttributeSources;
+
/**
* Constructs a new {@link ViewElementDescriptor} based on its XML name, UI name,
* the canonical name of the class it represents, its tooltip, its SDK url, its attributes list,
/**
* Returns the fully qualified name of the View class represented by this element descriptor
* e.g. "android.view.View".
+ *
+ * @return the fully qualified class name, never null
*/
public String getFullClassName() {
return mFullClassName;
}
- /** Returns the list of layout attributes. Can be empty but not null. */
+ /** Returns the list of layout attributes. Can be empty but not null.
+ *
+ * @return the list of layout attributes, never null
+ */
public AttributeDescriptor[] getLayoutAttributes() {
return mLayoutAttributes;
}
/**
* Returns the {@link ViewElementDescriptor} of the super-class of this View descriptor
* that matches the java View hierarchy. Can be null.
+ *
+ * @return the super class' descriptor or null
*/
public ViewElementDescriptor getSuperClassDesc() {
return mSuperClassDesc;
/**
* Sets the {@link ViewElementDescriptor} of the super-class of this View descriptor
* that matches the java View hierarchy. Can be null.
+ *
+ * @param superClassDesc the descriptor for the super class, or null
*/
public void setSuperClass(ViewElementDescriptor superClassDesc) {
mSuperClassDesc = superClassDesc;
}
/**
+ * Returns the list of attribute sources for the attributes provided by this
+ * descriptor. An attribute source is the fully qualified class name of the
+ * defining class for some of the properties. The specific attribute source
+ * of a given {@link AttributeInfo} can be found by calling
+ * {@link AttributeInfo#getDefinedBy()}.
+ * <p>
+ * The attribute sources are ordered from class to super class.
+ * <p>
+ * The list may <b>not</b> be modified by clients.
+ *
+ * @return a non null list of attribute sources for this view
+ */
+ public List<String> getAttributeSources() {
+ return mAttributeSources != null ? mAttributeSources : Collections.<String>emptyList();
+ }
+
+ /**
+ * Sets the attribute sources for this view. See {@link #getAttributes()}
+ * for details.
+ *
+ * @param attributeSources a non null list of attribute sources for this
+ * view descriptor
+ * @see #getAttributeSources()
+ */
+ public void setAttributeSources(List<String> attributeSources) {
+ mAttributeSources = attributeSources;
+ }
+
+ /**
* Returns true if views with the given fully qualified class name need to include
* their package in the layout XML tag
*
Set<String> availableIds = computeApplicableActionIds(allActions);
List<RuleAction> firstSelectedActions = allActions.get(mNodes.get(0));
+ int count = 0;
for (RuleAction firstAction : firstSelectedActions) {
if (!availableIds.contains(firstAction.getId())
&& !(firstAction instanceof RuleAction.Separator)) {
}
createContributionItem(firstAction, mNodes).fill(menu, -1);
+ count++;
+ }
+
+ if (count == 0) {
+ addDisabledMessageItem("<Empty>");
}
}
}
}
String title = titles.get(i);
- IAction a = new Action(title, IAction.AS_PUSH_BUTTON) {
+ IAction a = new Action(title,
+ current != null ? IAction.AS_CHECK_BOX : IAction.AS_PUSH_BUTTON) {
@Override
public void runWithEvent(Event event) {
run();
import java.util.Collection;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
public Margins getInsets() {
return mRulesEngine.getEditor().getCanvasControl().getInsets(fqcn);
}
+
+ public List<String> getTopAttributes() {
+ return ViewMetadataRepository.get().getTopAttributes(fqcn);
+ }
};
}
import org.w3c.dom.Node;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
return infos;
}
+ public List<String> getAttributeSources() {
+ ElementDescriptor descriptor = mNode.getDescriptor();
+ if (descriptor instanceof ViewElementDescriptor) {
+ return ((ViewElementDescriptor) descriptor).getAttributeSources();
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
public IAttribute[] getLiveAttributes() {
UiElementNode uiNode = mNode;
}
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);
+ renderMode, relatedTo, resize, topAttrs);
String init = child.getAttribute("init"); //$NON-NLS-1$
String icon = child.getAttribute("icon"); //$NON-NLS-1$
}
if (remaining.size() > 0) {
- List<ViewElementDescriptor> otherItems = new ArrayList<ViewElementDescriptor>(remaining);
+ List<ViewElementDescriptor> otherItems =
+ new ArrayList<ViewElementDescriptor>(remaining);
// Always sorted, we don't have a natural order for these unknowns
Collections.sort(otherItems);
if (createCategories) {
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 relatedTo, String resize, String topAttrs) {
super();
mFqcn = fqcn;
mDisplayName = displayName;
mRenderMode = renderMode;
mRelatedTo = relatedTo;
mResize = resize;
+ mTopAttrs = topAttrs;
}
/** Returns the {@link FillPreference} for views of this type */
}
}
+ 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);
}
/**
+ * 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
*
*/
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)) {
+ if ("alone".equals(render)) { //$NON-NLS-1$
return ALONE;
- } else if ("skip".equals(render)) {
+ } else if ("skip".equals(render)) { //$NON-NLS-1$
return SKIP;
} else {
return NORMAL;
render (alone|skip|normal) "normal"
fill (none|both|width|height|opposite|width_in_vertical|height_in_horizontal) "none"
resize (full|none|horizontal|vertical|scaled) "full"
+ topAttrs CDATA #IMPLIED
>
]>
<metadata>
name="Form Widgets">
<view
class="android.widget.TextView"
+ topAttrs="text,textAppearance,textColor,textSize"
name="TextView"
init=""
relatedTo="EditText,AutoCompleteTextView,MultiAutoCompleteTextView">
</view>
<view
class="android.widget.Button"
+ topAttrs="text,style"
relatedTo="ImageButton" />
<view
class="android.widget.ToggleButton"
+ topAttrs="textOff,textOn,style,background"
relatedTo="CheckBox" />
<view
class="android.widget.CheckBox"
+ topAttrs="text"
relatedTo="RadioButton,ToggleButton,CheckedTextView" />
<view
class="android.widget.RadioButton"
+ topAttrs="text,style"
relatedTo="CheckBox,ToggleButton" />
<view
class="android.widget.CheckedTextView"
+ topAttrs="gravity,paddingLeft,paddingRight,checkMark,textAppearance"
relatedTo="TextView,CheckBox" />
<view
class="android.widget.Spinner"
+ topAttrs="prompt,entries,style"
relatedTo="EditText"
fill="width_in_vertical" />
<view
class="android.widget.ProgressBar"
+ topAttrs="style,visibility,indeterminate,max"
relatedTo="SeekBar"
name="ProgressBar (Large)"
init="style=?android:attr/progressBarStyleLarge"
</view>
<view
class="android.widget.SeekBar"
+ topAttrs="paddingLeft,paddingRight,progressDrawable,thumb"
relatedTo="ProgressBar"
resize="horizontal"
fill="width_in_vertical" />
<view
class="android.widget.QuickContactBadge"
+ topAttrs="src,style,gravity"
resize="scaled" />
<view
- class="android.widget.RadioGroup" />
+ class="android.widget.RadioGroup"
+ topAttrs="orientation,paddingBottom,paddingTop,style" />
<view
class="android.widget.RatingBar"
+ topAttrs="numStars,stepSize,style,isIndicator"
resize="horizontal" />
</category>
<category
name="Text Fields">
<view
class="android.widget.EditText"
+ topAttrs="hint,inputType,singleLine"
name="Plain Text"
init=""
resize="full"
</view>
<view
class="android.widget.AutoCompleteTextView"
+ topAttrs="singleLine,autoText"
fill="width_in_vertical" />
<view
class="android.widget.MultiAutoCompleteTextView"
+ topAttrs="background,hint,imeOptions,inputType,style,textColor"
fill="width_in_vertical" />
</category>
<category
render="skip" />
<view
class="android.widget.LinearLayout"
+ topAttrs="orientation,gravity"
name="LinearLayout (Vertical)"
init="android:orientation=vertical"
icon="VerticalLinearLayout"
</view>
<view
class="android.widget.RelativeLayout"
+ topAttrs="background,orientation,paddingLeft"
fill="opposite"
render="skip" />
<view
class="android.widget.FrameLayout"
+ topAttrs="background"
fill="opposite"
render="skip" />
<view
class="include"
+ topAttrs="layout"
name="Include Other Layout"
render="skip" />
<view
class="fragment"
+ topAttrs="class,name"
name="Fragment"
fill="opposite"
render="skip" />
<view
class="android.widget.TableLayout"
+ topAttrs="stretchColumns,shrinkColumns,orientation"
fill="opposite"
render="skip" />
<view
class="android.widget.TableRow"
+ topAttrs="paddingTop,focusable,gravity,visibility"
fill="opposite"
resize="vertical"
render="skip" />
name="Composite">
<view
class="android.widget.ListView"
+ topAttrs="drawSelectorOnTop,cacheColorHint,divider,background"
relatedTo="ExpandableListView"
fill="width_in_vertical" />
<view
class="android.widget.ExpandableListView"
+ topAttrs="drawSelectorOnTop,cacheColorHint,indicatorLeft,indicatorRight,scrollbars,textSize"
relatedTo="ListView"
fill="width_in_vertical" />
<view
class="android.widget.GridView"
+ topAttrs="numColumns,verticalSpacing,horizontalSpacing"
fill="opposite"
render="skip" />
<view
class="android.widget.ScrollView"
+ topAttrs="fillViewport,orientation,scrollbars"
relatedTo="HorizontalScrollView"
fill="opposite"
render="skip" />
<view
class="android.widget.HorizontalScrollView"
+ topAttrs="scrollbars,fadingEdgeLength,fadingEdge"
relatedTo="ScrollView"
render="skip" />
<view
class="android.widget.SearchView"
+ topAttrs="iconifiedByDefault,queryHint,maxWidth,minWidth,visibility"
render="skip" />
<view
- class="android.widget.SlidingDrawer" />
+ class="android.widget.SlidingDrawer"
+ topAttrs="allowSingleTap,bottomOffset,content,handle,topOffset,visibility" />
<view
class="android.widget.TabHost"
+ topAttrs="paddingTop,background,duplicateParentState,visibility"
fill="width_in_vertical"
render="alone" />
<view
class="android.widget.TabWidget"
+ topAttrs="background,paddingLeft,tabStripEnabled,gravity"
render="alone" />
<view
class="android.webkit.WebView"
+ topAttrs="background,visibility,textAppearance"
fill="opposite"
render="skip" />
</category>
name="Images & Media">
<view
class="android.widget.ImageView"
+ topAttrs="src,scaleType"
resize="scaled"
relatedTo="ImageButton,VideoView" />
<view
class="android.widget.ImageButton"
+ topAttrs="src,background,style"
resize="scaled"
relatedTo="Button,ImageView" />
<view
class="android.widget.Gallery"
+ topAttrs="gravity,spacing,background"
fill="width_in_vertical"
render="skip" />
<view
name="Time & Date">
<view
class="android.widget.TimePicker"
+ topAttrs="visibility"
relatedTo="DatePicker,CalendarView"
render="alone" />
<view
render="alone" />
<view
class="android.widget.CalendarView"
+ topAttrs="focusable,focusableInTouchMode,visibility"
fill="both"
relatedTo="TimePicker,DatePicker" />
<view
class="android.widget.Chronometer"
+ topAttrs="textSize,gravity,visibility"
render="skip" />
<view
class="android.widget.AnalogClock"
+ topAttrs="dial,hand_hour,hand_minute"
relatedTo="DigitalClock" />
<view
class="android.widget.DigitalClock"
name="Transitions">
<view
class="android.widget.ImageSwitcher"
+ topAttrs="inAnimation,outAnimation,cropToPadding,padding,scaleType"
relatedTo="ViewFlipper,ViewSwitcher,TextSwitcher"
render="skip" />
<view
class="android.widget.AdapterViewFlipper"
+ topAttrs="autoStart,flipInterval,inAnimation,outAnimation"
fill="opposite"
render="skip" />
<view
class="android.widget.StackView"
+ topAttrs="loopViews,gravity"
fill="opposite"
render="skip" />
<view
render="skip" />
<view
class="android.widget.ViewAnimator"
+ topAttrs="inAnimation,outAnimation"
fill="opposite"
render="skip" />
<view
class="android.widget.ViewFlipper"
+ topAttrs="flipInterval,inAnimation,outAnimation,addStatesFromChildren,measureAllChildren"
relatedTo="ViewSwitcher,ImageSwitcher,TextSwitcher"
fill="opposite"
render="skip" />
<view
class="android.widget.ViewSwitcher"
+ topAttrs="inAnimation,outAnimation"
relatedTo="ViewFlipper,ImageSwitcher,TextSwitcher"
fill="opposite"
render="skip" />
render="skip" />
<view
class="android.view.View"
+ topAttrs="background,visibility,style"
render="skip" />
<view
class="android.view.ViewStub"
+ topAttrs="layout,inflatedId,visibility"
render="skip" />
<view
class="android.gesture.GestureOverlayView"
+ topAttrs="gestureStrokeType,uncertainGestureColor,eventsInterceptionEnabled,gestureColor,orientation"
render="skip" />
<view
class="android.view.TextureView"
render="skip" />
<view
class="android.widget.NumberPicker"
+ topAttrs="focusable,focusableInTouchMode"
relatedTo="TimePicker,DatePicker"
render="alone" />
<view
class="android.widget.ZoomButton"
+ topAttrs="background"
relatedTo="Button,ZoomControls" />
<view
class="android.widget.ZoomControls"
+ topAttrs="style,background,gravity"
relatedTo="ZoomButton"
resize="none" />
<view
class="merge"
+ topAttrs="orientation,gravity,style"
skip="true"
render="skip" />
<view
render="skip" />
<view
class="android.widget.TwoLineListItem"
+ topAttrs="mode,paddingBottom,paddingTop,minHeight,paddingLeft"
render="skip" />
<view
class="android.widget.AbsoluteLayout"
+ topAttrs="background,orientation,paddingBottom,paddingLeft,paddingRight,paddingTop"
name="AbsoluteLayout (Deprecated)"
fill="opposite"
render="skip" />
import junit.framework.TestCase;
public class BaseViewRuleTest extends TestCase {
- public final void testPrettyName() {
- assertEquals(null, BaseViewRule.prettyName(null));
- assertEquals("", BaseViewRule.prettyName(""));
- assertEquals("Foo", BaseViewRule.prettyName("foo"));
- assertEquals("Foo bar", BaseViewRule.prettyName("foo_bar"));
- // TODO: We should check this to capitalize each initial word
- // assertEquals("Foo Bar", BaseView.prettyName("foo_bar"));
- // TODO: We should also handle camelcase properties
- // assertEquals("Foo Bar", BaseView.prettyName("fooBar"));
+
+ public final void testGetAttributeDisplayName() {
+ assertEquals(null, BaseViewRule.getAttributeDisplayName(null));
+ assertEquals("", BaseViewRule.getAttributeDisplayName(""));
+ assertEquals("Foo", BaseViewRule.getAttributeDisplayName("foo"));
+ assertEquals("FooBar", BaseViewRule.getAttributeDisplayName("fooBar"));
+ assertEquals("Foo Bar", BaseViewRule.getAttributeDisplayName("foo_bar"));
+ // TBD: Should we also handle CamelCase properties?
+ // assertEquals("Foo Bar", BaseViewRule.getAttributeDisplayName("fooBar"));
}
public final void testJoin() {
import com.android.ide.common.api.IValidator;
import com.android.ide.common.api.IViewMetadata;
import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.Margins;
import com.android.ide.common.api.Point;
import com.android.ide.common.api.Rect;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
import java.util.ArrayList;
import java.util.Collection;
return mFqn;
}
- public IViewMetadata getMetadata(String fqcn) {
- fail("Not supported in tests yet");
- return null;
+ public IViewMetadata getMetadata(final String fqcn) {
+ return new IViewMetadata() {
+ public String getDisplayName() {
+ // This also works when there is no "."
+ return fqcn.substring(fqcn.lastIndexOf('.') + 1);
+ }
+
+ public FillPreference getFillPreference() {
+ return ViewMetadataRepository.get().getFillPreference(fqcn);
+ }
+
+ public Margins getInsets() {
+ return null;
+ }
+
+ public List<String> getTopAttributes() {
+ return ViewMetadataRepository.get().getTopAttributes(fqcn);
+ }
+ };
}
public int getMinApiLevel() {
import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL;
import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IAttributeInfo.Format;
import com.android.ide.common.api.IDragElement;
import com.android.ide.common.api.IMenuCallback;
import com.android.ide.common.api.INode;
import com.android.ide.common.api.IViewRule;
-import com.android.ide.common.api.RuleAction;
import com.android.ide.common.api.Point;
import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.RuleAction;
+import com.android.ide.common.api.RuleAction.NestedAction;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
rule.addContextMenuActions(contextMenu, node);
assertEquals(6, contextMenu.size());
assertEquals("Edit ID...", contextMenu.get(0).getTitle());
- assertEquals("Layout Width", contextMenu.get(1).getTitle());
- assertEquals("Layout Height", contextMenu.get(2).getTitle());
- assertTrue(contextMenu.get(3) instanceof RuleAction.Separator);
- assertEquals("Properties", contextMenu.get(4).getTitle());
- assertEquals("Orientation", contextMenu.get(5).getTitle());
+ assertTrue(contextMenu.get(1) instanceof RuleAction.Separator);
+ assertEquals("Layout Width", contextMenu.get(2).getTitle());
+ assertEquals("Layout Height", contextMenu.get(3).getTitle());
+ assertTrue(contextMenu.get(4) instanceof RuleAction.Separator);
+ assertEquals("Other Properties", contextMenu.get(5).getTitle());
- RuleAction propertiesMenu = contextMenu.get(4);
+ RuleAction propertiesMenu = contextMenu.get(5);
assertTrue(propertiesMenu.getClass().getName(),
- propertiesMenu instanceof RuleAction.NestedAction);
- // TODO: Test Properties-list
+ propertiesMenu instanceof NestedAction);
}
public void testContextMenuCustom() {
LinearLayoutRule rule = new LinearLayoutRule();
initialize(rule, "android.widget.LinearLayout");
- INode node = TestNode.create("android.widget.Button").id("@+id/Button012")
+ INode node = TestNode.create("android.widget.LinearLayout").id("@+id/LinearLayout")
.set(ANDROID_URI, ATTR_LAYOUT_WIDTH, "42dip")
.set(ANDROID_URI, ATTR_LAYOUT_HEIGHT, "50sp");
List<RuleAction> contextMenu = new ArrayList<RuleAction>();
rule.addContextMenuActions(contextMenu, node);
assertEquals(6, contextMenu.size());
- assertEquals("Layout Width", contextMenu.get(1).getTitle());
- RuleAction menuAction = contextMenu.get(1);
+ assertEquals("Layout Width", contextMenu.get(2).getTitle());
+ RuleAction menuAction = contextMenu.get(2);
assertTrue(menuAction instanceof RuleAction.Choices);
RuleAction.Choices choices = (RuleAction.Choices) menuAction;
List<String> titles = choices.getTitles();
public void testOrientation() {
LinearLayoutRule rule = new LinearLayoutRule();
initialize(rule, "android.widget.LinearLayout");
- INode node = TestNode.create("android.widget.Button").id("@+id/Button012");
+ TestNode node = TestNode.create("android.widget.LinearLayout").id("@+id/LinearLayout012");
+ node.putAttributeInfo(ANDROID_URI, "orientation",
+ new TestAttributeInfo(ATTR_ORIENTATION, new Format[] { Format.ENUM },
+ "android.widget.LinearLayout",
+ new String[] {"horizontal", "vertical"}, null, null));
assertNull(node.getStringAttr(ANDROID_URI, ATTR_ORIENTATION));
List<RuleAction> contextMenu = new ArrayList<RuleAction>();
rule.addContextMenuActions(contextMenu, node);
- assertEquals(6, contextMenu.size());
- RuleAction orientationAction = contextMenu.get(5);
+ assertEquals(7, contextMenu.size());
+ RuleAction orientationAction = contextMenu.get(1);
assertEquals("Orientation", orientationAction.getTitle());
assertTrue(orientationAction.getClass().getName(),
assertEquals(VALUE_HORIZONTAL, orientation);
}
+ // Check that the context menu manipulates the orientation attribute
+ public void testProperties() {
+ LinearLayoutRule rule = new LinearLayoutRule();
+ initialize(rule, "android.widget.LinearLayout");
+ TestNode node = TestNode.create("android.widget.LinearLayout").id("@+id/LinearLayout012");
+ node.putAttributeInfo(ANDROID_URI, "orientation",
+ new TestAttributeInfo(ATTR_ORIENTATION, new Format[] { Format.ENUM },
+ "android.widget.LinearLayout",
+ new String[] {"horizontal", "vertical"}, null, null));
+ node.setAttributeSources(Arrays.asList("android.widget.LinearLayout",
+ "android.view.ViewGroup", "android.view.View"));
+ node.putAttributeInfo(ANDROID_URI, "gravity",
+ new TestAttributeInfo("gravity", new Format[] { Format.INTEGER },
+ "android.widget.LinearLayout", null, null, null));
+
+
+ assertNull(node.getStringAttr(ANDROID_URI, ATTR_ORIENTATION));
+
+ List<RuleAction> contextMenu = new ArrayList<RuleAction>();
+ rule.addContextMenuActions(contextMenu, node);
+ assertEquals(8, contextMenu.size());
+
+ assertEquals("Orientation", contextMenu.get(1).getTitle());
+ assertEquals("Edit Gravity...", contextMenu.get(2).getTitle());
+
+ assertEquals("Other Properties", contextMenu.get(7).getTitle());
+
+ RuleAction propertiesMenu = contextMenu.get(7);
+ assertTrue(propertiesMenu.getClass().getName(),
+ propertiesMenu instanceof NestedAction);
+ NestedAction nested = (NestedAction) propertiesMenu;
+ List<RuleAction> nestedActions = nested.getNestedActions(node);
+ assertEquals(9, nestedActions.size());
+ assertEquals("Recent", nestedActions.get(0).getTitle());
+ assertTrue(nestedActions.get(1) instanceof RuleAction.Separator);
+ assertEquals("Defined by LinearLayout", nestedActions.get(2).getTitle());
+ assertEquals("Inherited from ViewGroup", nestedActions.get(3).getTitle());
+ assertEquals("Inherited from View", nestedActions.get(4).getTitle());
+ assertTrue(nestedActions.get(5) instanceof RuleAction.Separator);
+ assertEquals("Layout Parameters", nestedActions.get(6).getTitle());
+ assertTrue(nestedActions.get(7) instanceof RuleAction.Separator);
+ assertEquals("All By Name", nestedActions.get(8).getTitle());
+
+ BaseViewRule.editedProperty(ATTR_ORIENTATION);
+
+ RuleAction recentAction = nestedActions.get(0);
+ assertTrue(recentAction instanceof NestedAction);
+ NestedAction recentChoices = (NestedAction) recentAction;
+ List<RuleAction> recentItems = recentChoices.getNestedActions(node);
+
+ assertEquals(1, recentItems.size());
+ assertEquals("Orientation", recentItems.get(0).getTitle());
+
+ BaseViewRule.editedProperty("gravity");
+ recentItems = recentChoices.getNestedActions(node);
+ assertEquals(2, recentItems.size());
+ assertEquals("Gravity...", recentItems.get(0).getTitle());
+ assertEquals("Orientation", recentItems.get(1).getTitle());
+
+ BaseViewRule.editedProperty(ATTR_ORIENTATION);
+ recentItems = recentChoices.getNestedActions(node);
+ assertEquals(2, recentItems.size());
+ assertEquals("Orientation", recentItems.get(0).getTitle());
+ assertEquals("Gravity...", recentItems.get(1).getTitle());
+
+ // Lots of other properties -- flushes out properties that apply to this view
+ for (int i = 0; i < 30; i++) {
+ BaseViewRule.editedProperty("dummy_" + i);
+ }
+ recentItems = recentChoices.getNestedActions(node);
+ assertEquals(0, recentItems.size());
+
+ BaseViewRule.editedProperty("gravity");
+ recentItems = recentChoices.getNestedActions(node);
+ assertEquals(1, recentItems.size());
+ assertEquals("Gravity...", recentItems.get(0).getTitle());
+ }
+
public void testDragInEmptyWithBounds() {
dragIntoEmpty(new Rect(0, 0, 100, 80));
}
*/
package com.android.ide.common.layout;
-import static junit.framework.Assert.fail;
-
import com.android.ide.common.api.IAttributeInfo;
/** Test/mock implementation of {@link IAttributeInfo} */
public class TestAttributeInfo implements IAttributeInfo {
private final String mName;
+ private final Format[] mFormats;
+ private final String mDefinedBy;
+ private final String[] mEnumValues;
+ private final String[] mFlagValues;
+ private final String mJavadoc;
public TestAttributeInfo(String name) {
+ this(name, null, null, null, null, null);
+ }
+
+ public TestAttributeInfo(String name, Format[] formats, String definedBy,
+ String[] enumValues, String[] flagValues, String javadoc) {
+ super();
this.mName = name;
+ this.mFormats = formats;
+ this.mDefinedBy = definedBy;
+ this.mEnumValues = enumValues;
+ this.mFlagValues = flagValues;
+ this.mJavadoc = javadoc;
}
public String getDeprecatedDoc() {
- fail("Not supported yet in tests");
return null;
}
public String[] getEnumValues() {
- fail("Not supported yet in tests");
- return null;
+ return mEnumValues;
}
public String[] getFlagValues() {
- fail("Not supported yet in tests");
- return null;
+ return mFlagValues;
}
public Format[] getFormats() {
- fail("Not supported yet in tests");
- return null;
+ return mFormats;
}
public String getJavaDoc() {
- fail("Not supported yet in tests");
- return null;
+ return mJavadoc;
}
public String getName() {
return mName;
}
+
+ public String getDefinedBy() {
+ return mDefinedBy;
+ }
}
\ No newline at end of file
import com.android.ide.common.api.Rect;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
private Map<String, IAttributeInfo> mAttributeInfos = new HashMap<String, IAttributeInfo>();
+ private List<String> mAttributeSources;
+
public TestNode(String fqcn) {
this.mFqcn = fqcn;
}
callback.handle(this);
}
+ public void putAttributeInfo(String uri, String attrName, IAttributeInfo info) {
+ mAttributeInfos.put(uri + attrName, info);
+ }
+
public IAttributeInfo getAttributeInfo(String uri, String attrName) {
return mAttributeInfos.get(uri + attrName);
}
public Margins getMargins() {
return null;
}
+
+ public List<String> getAttributeSources() {
+ return mAttributeSources != null ? mAttributeSources : Collections.<String>emptyList();
+ }
+
+ public void setAttributeSources(List<String> attributeSources) {
+ mAttributeSources = attributeSources;
+ }
}
\ No newline at end of file
import com.android.ide.common.api.IViewMetadata.FillPreference;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository.RenderMode;
+import java.util.Arrays;
+
import junit.framework.TestCase;
public class ViewMetadataRepositoryTest extends TestCase {
assertEquals(RenderMode.SKIP, repository.getRenderMode("android.widget.LinearLayout"));
assertEquals(RenderMode.ALONE, repository.getRenderMode("android.widget.TabHost"));
}
+
+ public void testGetTopAttributes() throws Exception {
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ assertEquals(Arrays.asList("id", "text", "style"),
+ repository.getTopAttributes("android.widget.RadioButton"));
+ assertEquals(Arrays.asList("id", "gravity", "paddingLeft", "paddingRight", "checkMark",
+ "textAppearance"),
+ repository.getTopAttributes("android.widget.CheckedTextView"));
+ assertEquals(Arrays.asList("id"),
+ repository.getTopAttributes("android.widget.NonExistent"));
+ }
+
}
/** Returns the documentation for deprecated attributes. Null if not deprecated. */
public String getDeprecatedDoc();
+ /** Returns the fully qualified class name of the view defining this attribute */
+ public String getDefinedBy();
}
import com.android.ide.common.api.IDragElement.IDragAttribute;
+import java.util.List;
+
/**
* Represents a view in the XML layout being edited.
* If you want attributes actually written in the XML and their values, please use
* {@link #getStringAttr(String, String)} or {@link #getLiveAttributes()} instead.
*
- * @return A non-null possible-empty list of {@link IAttributeInfo}.
+ * @return A non-null possibly-empty list of {@link IAttributeInfo}.
*/
public IAttributeInfo[] getDeclaredAttributes();
/**
+ * Returns the list of classes (fully qualified class names) that are
+ * contributing properties to the {@link #getDeclaredAttributes()} attribute
+ * list, in order from most specific to least specific (in other words,
+ * android.view.View will be last in the list.) This is usually the same as
+ * the super class chain of a view, but it skips any views that do not
+ * contribute attributes.
+ *
+ * @return a list of views classes that contribute attributes to this node,
+ * which is never null because at least android.view.View will
+ * contribute attributes.
+ */
+ public List<String> getAttributeSources();
+
+ /**
* Returns the list of all attributes defined in the XML for this node.
* <p/>
* This looks up an attribute in the <em>current</em> XML source, not the in-memory model.
* If you want a list of all possible attributes, whether used in the XML or not by
* this node, please see {@link #getDeclaredAttributes()} instead.
*
- * @return A non-null possible-empty list of {@link IAttribute}.
+ * @return A non-null possibly-empty list of {@link IAttribute}.
*/
public IAttribute[] getLiveAttributes();
package com.android.ide.common.api;
+import java.util.List;
+
/**
* Metadata about a particular view. The metadata for a View can be found by asking the
* {@link IClientRulesEngine} for the metadata for a given class via
public FillPreference getFillPreference();
/**
+ * Returns the most common attributes for this view.
+ *
+ * @return a list of attribute names (not including a namespace prefix) that
+ * are commonly set for this type of view, never null
+ */
+ public List<String> getTopAttributes();
+
+ /**
* Types of fill behavior that views can prefer.
* <p>
* TODO: Consider better names. FillPolicy? Stretchiness?