OSDN Git Service

Layout editor property menu improvements
authorTor Norbye <tnorbye@google.com>
Mon, 15 Aug 2011 23:38:30 +0000 (16:38 -0700)
committerTor Norbye <tnorbye@google.com>
Fri, 19 Aug 2011 23:43:08 +0000 (16:43 -0700)
This changeset adds two forms of view attribute metadata:
* First, it records the most commonly used attributes for each
  view. This was determined by gathering statistics on as many layout
  files as I could find and then picking those that are used 10% or
  more.

* Second, it records in the attribute metadata which View defines a
  given attribute.

The context menu uses the above information to present the available
attributes in several ways:

* In the top level menu, where we had "Edit ID", and if applicable
  "Edit Text", it now lists the top attributes instead. For example,
  for a RatingBar the first handful of menu options are "Edit ID...",
  "Edit NumStars...", "Edit StepSize...", "Edit Style..." and
  "IsIndicator" (a boolean pull-right menu).

  Incidentally this automatically handles some cases which were
  manually handled before, so the code in LinearLayoutRule to add an
  "Orientation" menu is no longer needed; it's just one of the two
  common attributes handled by the new attribute list.

* The "Properties" menu is now called "Other Properties", and instead
  of showing all properties, it has a new level of menus:

  * "Recent". This is initially empty, but as you edit other attributes,
    it gets populated (in most recently used order, kept up to date)
    with recently edited properties.

  * One submenu for each defining View super class listing exactly
    the attributes defined by that view. This is useful for browsing
    and editing related attributes. If you are looking at a textual
    view like a Button for example, you can look at the "TextView"
    menu to find all the text related options (TextColor, TextSize,
    etc).  These menus are listed from the nearest to the further
    superclass, so for example if you right click on a CalendarView
    you'll see these menus:

       Recent                     >
       ----------------------------
       Defined by CalendarView    >
       Inherited from FrameLayout >
       Inherited from ViewGroup   >
       Inherited from View        >
       ----------------------------
       Layout Parameters          >
       ----------------------------
       All By Name                >

  * As you can see from the above, there are two more menus below the
    inherited menu items. "Layout Parameters" lists all the layout
    parameters available for the selected nodes (which is defined not
    by the view itself but the view that it is contained within).  And
    finally there is "All By Name", which is a complete menu
    containing all available attributes for the view (and this is what
    the Properties menu used to contain).

* The code which computes a display name from an attribute was also
  tweaked to capitalize not just the first letter but any first word
  letter, so for example when you look at the possible values for
  Gravity you now see "Clip Vertical" instead of "Clip vertical".

* The edit property dialog for the properties menus now uses @string
  or @style resource choosers for the text, hint and style attributes
  (used to just be a plain text box.)

Change-Id: I3b30d48b85fd13f0190c760756bf383a47b3f4a5

31 files changed:
attribute_stats/.classpath [new file with mode: 0644]
attribute_stats/.gitignore [new file with mode: 0644]
attribute_stats/.project [new file with mode: 0644]
attribute_stats/.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
attribute_stats/README.txt [new file with mode: 0644]
attribute_stats/src/Analyzer.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/EditTextRule.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttributeInfo.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttrsXmlParser.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/DeclareStyleableInfo.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/LayoutDescriptors.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/ViewElementDescriptor.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml
eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/BaseViewRuleTest.java
eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java
eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LinearLayoutRuleTest.java
eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestAttributeInfo.java
eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java
eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepositoryTest.java
rule_api/src/com/android/ide/common/api/IAttributeInfo.java
rule_api/src/com/android/ide/common/api/INode.java
rule_api/src/com/android/ide/common/api/IViewMetadata.java

diff --git a/attribute_stats/.classpath b/attribute_stats/.classpath
new file mode 100644 (file)
index 0000000..fb50116
--- /dev/null
@@ -0,0 +1,6 @@
+<?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>
diff --git a/attribute_stats/.gitignore b/attribute_stats/.gitignore
new file mode 100644 (file)
index 0000000..ba077a4
--- /dev/null
@@ -0,0 +1 @@
+bin
diff --git a/attribute_stats/.project b/attribute_stats/.project
new file mode 100644 (file)
index 0000000..2f2cff1
--- /dev/null
@@ -0,0 +1,17 @@
+<?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>
diff --git a/attribute_stats/.settings/org.eclipse.jdt.core.prefs b/attribute_stats/.settings/org.eclipse.jdt.core.prefs
new file mode 100644 (file)
index 0000000..e755df2
--- /dev/null
@@ -0,0 +1,71 @@
+#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
diff --git a/attribute_stats/README.txt b/attribute_stats/README.txt
new file mode 100644 (file)
index 0000000..15f51c2
--- /dev/null
@@ -0,0 +1,13 @@
+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.
diff --git a/attribute_stats/src/Analyzer.java b/attribute_stats/src/Analyzer.java
new file mode 100644 (file)
index 0000000..a6bbb4a
--- /dev/null
@@ -0,0 +1,582 @@
+/*
+ * 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;
+        }
+    }
+}
index ab64ef1..c0a8f53 100644 (file)
@@ -160,7 +160,7 @@ public class BaseLayoutRule extends BaseViewRule {
                         // 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);
                         }
                     }
index 66688d9..dcf0f14 100644 (file)
 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;
@@ -36,6 +40,7 @@ import com.android.ide.common.api.IGraphics;
 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;
@@ -43,8 +48,8 @@ import com.android.ide.common.api.Rect;
 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;
@@ -55,6 +60,7 @@ import java.util.Collections;
 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;
@@ -64,16 +70,17 @@ import java.util.Set;
  * 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;
@@ -159,7 +166,7 @@ public class BaseViewRule implements IViewRule {
                 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) {
@@ -167,9 +174,10 @@ public class BaseViewRule implements IViewRule {
                                     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) {
@@ -178,9 +186,10 @@ public class BaseViewRule implements IViewRule {
                                     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) {
@@ -195,80 +204,91 @@ public class BaseViewRule implements IViewRule {
                             }
                             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);
                                 }
                             }
                         }
@@ -332,13 +352,16 @@ public class BaseViewRule implements IViewRule {
 
         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) {
@@ -351,11 +374,11 @@ public class BaseViewRule implements IViewRule {
         }
         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));
 
@@ -372,7 +395,7 @@ public class BaseViewRule implements IViewRule {
         }
         heightChoices.add(Pair.of(ZCUSTOM, "Other..."));
         actions.add(RuleAction.createChoices(
-                HEIGHT_ID, "Layout Height",
+                ATTR_LAYOUT_HEIGHT, "Layout Height",
                 onChange,
                 null /* iconUrls */,
                 currentHeight,
@@ -381,14 +404,52 @@ public class BaseViewRule implements IViewRule {
                 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;
             }
         });
 
@@ -409,13 +470,201 @@ public class BaseViewRule implements IViewRule {
     }
 
     /**
+     * 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
@@ -431,91 +680,38 @@ public class BaseViewRule implements IViewRule {
                     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;
     }
 
     /**
@@ -627,9 +823,31 @@ public class BaseViewRule implements IViewRule {
         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;
@@ -698,16 +916,23 @@ public class BaseViewRule implements IViewRule {
         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() {
@@ -760,6 +985,12 @@ public class BaseViewRule implements IViewRule {
     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$
index e26df79..a87de29 100644 (file)
@@ -76,6 +76,7 @@ public class EditTextRule extends BaseViewRule {
 
         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 */
index d4ed864..7b6081d 100644 (file)
@@ -176,6 +176,9 @@ public class LayoutConstants {
     /** 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$
 
index ae42fc3..3639648 100644 (file)
@@ -44,14 +44,13 @@ import com.android.ide.common.api.IViewMetadata;
 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;
@@ -89,33 +88,6 @@ public class LinearLayoutRule extends BaseLayoutRule {
             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
index 6f58656..a761a0e 100755 (executable)
@@ -38,6 +38,8 @@ public class AttributeInfo implements IAttributeInfo {
     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
@@ -117,4 +119,26 @@ public class AttributeInfo implements IAttributeInfo {
     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;
+    }
 }
index f8d041c..ed2fb75 100644 (file)
@@ -16,6 +16,8 @@
 
 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;
@@ -162,7 +164,14 @@ public final class AttrsXmlParser {
             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());
             }
         }
@@ -174,14 +183,24 @@ public final class AttrsXmlParser {
     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);
             }
         }
     }
index 8719aa9..40111e2 100644 (file)
@@ -24,12 +24,12 @@ package com.android.ide.common.resources.platform;
  */
 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;
 
     /**
@@ -70,7 +70,6 @@ public class DeclareStyleableInfo {
         }
     }
 
-
     /** Returns style name */
     public String getStyleName() {
         return mStyleName;
@@ -81,11 +80,6 @@ public class DeclareStyleableInfo {
         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;
index ce3d59a..7572120 100644 (file)
@@ -296,7 +296,7 @@ public class ElementDescriptor implements Comparable<ElementDescriptor> {
         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) {
index 3bfcb5c..ca0475d 100644 (file)
@@ -281,6 +281,7 @@ public final class LayoutDescriptors implements IDescriptorProvider {
                 styleInfo,
                 false,      //required
                 null);      // overrides
+        styleInfo.setDefinedBy(SdkConstants.CLASS_VIEW);
 
         // Process all View attributes
         DescriptorsUtils.appendAttributes(attributes,
@@ -290,11 +291,17 @@ public final class LayoutDescriptors implements IDescriptorProvider {
                 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,
@@ -318,14 +325,16 @@ public final class LayoutDescriptors implements IDescriptorProvider {
                     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));
@@ -350,6 +359,7 @@ public final class LayoutDescriptors implements IDescriptorProvider {
                 layoutAttributes.toArray(new AttributeDescriptor[layoutAttributes.size()]),
                 null, // children
                 false /* mandatory */);
+        desc.setAttributeSources(Collections.unmodifiableList(attributeSources));
         infoDescMap.put(info, desc);
         return desc;
     }
index a18b821..fdfe191 100644 (file)
 
 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;
@@ -29,6 +30,9 @@ import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
 
 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.
@@ -62,6 +66,9 @@ public class ViewElementDescriptor extends ElementDescriptor {
     /** 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,
@@ -110,12 +117,17 @@ public class ViewElementDescriptor extends ElementDescriptor {
     /**
      * 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;
     }
@@ -141,6 +153,8 @@ public class ViewElementDescriptor extends ElementDescriptor {
     /**
      * 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;
@@ -149,6 +163,8 @@ public class ViewElementDescriptor extends ElementDescriptor {
     /**
      * 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;
@@ -183,6 +199,35 @@ public class ViewElementDescriptor extends ElementDescriptor {
     }
 
     /**
+     * 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
      *
index dfc30fe..7a2f7d5 100644 (file)
@@ -469,6 +469,7 @@ class DynamicContextMenu {
             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)) {
@@ -477,6 +478,11 @@ class DynamicContextMenu {
                 }
 
                 createContributionItem(firstAction, mNodes).fill(menu, -1);
+                count++;
+            }
+
+            if (count == 0) {
+                addDisabledMessageItem("<Empty>");
             }
         }
     }
@@ -546,7 +552,8 @@ class DynamicContextMenu {
                 }
 
                 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();
index d213646..dd24322 100644 (file)
@@ -88,6 +88,7 @@ import org.eclipse.ui.dialogs.SelectionDialog;
 
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicReference;
 
@@ -168,6 +169,10 @@ class ClientRulesEngine implements IClientRulesEngine {
             public Margins getInsets() {
                 return mRulesEngine.getEditor().getCanvasControl().getInsets(fqcn);
             }
+
+            public List<String> getTopAttributes() {
+                return ViewMetadataRepository.get().getTopAttributes(fqcn);
+            }
         };
     }
 
index b27954e..f29283e 100755 (executable)
@@ -43,6 +43,7 @@ import org.w3c.dom.NamedNodeMap;
 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;
@@ -387,6 +388,15 @@ public class NodeProxy implements INode {
         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;
 
index 61750f9..5b4b734 100644 (file)
@@ -268,10 +268,11 @@ public class ViewMetadataRepository {
         }
 
         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$
@@ -384,7 +385,8 @@ public class ViewMetadataRepository {
         }
 
         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) {
@@ -475,11 +477,13 @@ public class ViewMetadataRepository {
         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;
@@ -488,6 +492,7 @@ public class ViewMetadataRepository {
             mRenderMode = renderMode;
             mRelatedTo = relatedTo;
             mResize = resize;
+            mTopAttrs = topAttrs;
         }
 
         /** Returns the {@link FillPreference} for views of this type */
@@ -548,6 +553,22 @@ public class ViewMetadataRepository {
             }
         }
 
+        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);
@@ -661,6 +682,23 @@ public class ViewMetadataRepository {
     }
 
     /**
+     * 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
      *
@@ -692,10 +730,17 @@ public class ViewMetadataRepository {
          */
         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;
index 511d775..5a0a887 100644 (file)
@@ -26,6 +26,7 @@
     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>
@@ -33,6 +34,7 @@
         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 &amp; 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 &amp; 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" />
index d19e9bd..6d46b1e 100644 (file)
@@ -22,15 +22,15 @@ import java.util.Collections;
 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() {
index 3fee553..c325a40 100644 (file)
@@ -26,8 +26,10 @@ 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.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;
@@ -211,9 +213,25 @@ public class LayoutTestBase extends TestCase {
             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() {
index 145be61..4a0fc6e 100644 (file)
@@ -25,15 +25,18 @@ import static com.android.ide.common.layout.LayoutConstants.VALUE_HORIZONTAL;
 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;
@@ -130,30 +133,29 @@ public class LinearLayoutRuleTest extends LayoutTestBase {
         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();
@@ -171,14 +173,18 @@ public class LinearLayoutRuleTest extends LayoutTestBase {
     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(),
@@ -197,6 +203,84 @@ public class LinearLayoutRuleTest extends LayoutTestBase {
         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));
     }
index 0f2ab21..908d0ba 100644 (file)
  */
 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
index 7d77252..ed2bc43 100644 (file)
@@ -25,6 +25,7 @@ import com.android.ide.common.api.Margins;
 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;
@@ -43,6 +44,8 @@ public class TestNode implements INode {
 
     private Map<String, IAttributeInfo> mAttributeInfos = new HashMap<String, IAttributeInfo>();
 
+    private List<String> mAttributeSources;
+
     public TestNode(String fqcn) {
         this.mFqcn = fqcn;
     }
@@ -98,6 +101,10 @@ public class TestNode implements INode {
         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);
     }
@@ -180,4 +187,12 @@ public class TestNode implements INode {
     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
index 5921e85..50d438c 100644 (file)
@@ -18,6 +18,8 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gre;
 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 {
@@ -60,4 +62,16 @@ 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"));
+    }
+
 }
index 2a6ecd8..da1bc9e 100755 (executable)
@@ -81,4 +81,6 @@ public interface IAttributeInfo {
     /** 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();
 }
index e3f34a9..b4cb638 100755 (executable)
@@ -19,6 +19,8 @@ package com.android.ide.common.api;
 
 import com.android.ide.common.api.IDragElement.IDragAttribute;
 
+import java.util.List;
+
 
 /**
  * Represents a view in the XML layout being edited.
@@ -217,11 +219,25 @@ public interface INode {
      * 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.
@@ -232,7 +248,7 @@ public interface INode {
      * 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();
 
index 0687f30..8646764 100644 (file)
@@ -16,6 +16,8 @@
 
 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
@@ -49,6 +51,14 @@ public interface IViewMetadata {
     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?