diff options
31 files changed, 1534 insertions, 251 deletions
diff --git a/attribute_stats/.classpath b/attribute_stats/.classpath new file mode 100644 index 0000000..fb50116 --- /dev/null +++ b/attribute_stats/.classpath @@ -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 index 0000000..ba077a4 --- /dev/null +++ b/attribute_stats/.gitignore @@ -0,0 +1 @@ +bin diff --git a/attribute_stats/.project b/attribute_stats/.project new file mode 100644 index 0000000..2f2cff1 --- /dev/null +++ b/attribute_stats/.project @@ -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 index 0000000..e755df2 --- /dev/null +++ b/attribute_stats/.settings/org.eclipse.jdt.core.prefs @@ -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 index 0000000..15f51c2 --- /dev/null +++ b/attribute_stats/README.txt @@ -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 index 0000000..a6bbb4a --- /dev/null +++ b/attribute_stats/src/Analyzer.java @@ -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; + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java index ab64ef1..c0a8f53 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java @@ -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); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java index 66688d9..dcf0f14 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java @@ -17,10 +17,14 @@ 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$ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/EditTextRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/EditTextRule.java index e26df79..a87de29 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/EditTextRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/EditTextRule.java @@ -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 */ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java index d4ed864..7b6081d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java @@ -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$ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java index ae42fc3..3639648 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java @@ -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 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttributeInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttributeInfo.java index 6f58656..a761a0e 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttributeInfo.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttributeInfo.java @@ -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; + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttrsXmlParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttrsXmlParser.java index f8d041c..ed2fb75 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttrsXmlParser.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttrsXmlParser.java @@ -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); } } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/DeclareStyleableInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/DeclareStyleableInfo.java index 8719aa9..40111e2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/DeclareStyleableInfo.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/DeclareStyleableInfo.java @@ -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; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java index ce3d59a..7572120 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java @@ -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) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/LayoutDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/LayoutDescriptors.java index 3bfcb5c..ca0475d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/LayoutDescriptors.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/LayoutDescriptors.java @@ -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; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/ViewElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/ViewElementDescriptor.java index a18b821..fdfe191 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/ViewElementDescriptor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/ViewElementDescriptor.java @@ -16,10 +16,11 @@ 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 * diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java index dfc30fe..7a2f7d5 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java @@ -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(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java index d213646..dd24322 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java @@ -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); + } }; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java index b27954e..f29283e 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java @@ -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; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java index 61750f9..5b4b734 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java @@ -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; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml index 511d775..5a0a887 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml @@ -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"> @@ -48,25 +50,32 @@ </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" @@ -86,22 +95,27 @@ </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" @@ -148,9 +162,11 @@ </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 @@ -161,6 +177,7 @@ render="skip" /> <view class="android.widget.LinearLayout" + topAttrs="orientation,gravity" name="LinearLayout (Vertical)" init="android:orientation=vertical" icon="VerticalLinearLayout" @@ -171,27 +188,33 @@ </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" /> @@ -204,39 +227,49 @@ 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> @@ -244,14 +277,17 @@ name="Images & Media"> <view class="android.widget.ImageView" + topAttrs="src,scaleType" resize="scaled" relatedTo="ImageButton,VideoView" /> <view class="android.widget.ImageButton" + topAttrs="src,background,style" resize="scaled" relatedTo="Button,ImageView" /> <view class="android.widget.Gallery" + topAttrs="gravity,spacing,background" fill="width_in_vertical" render="skip" /> <view @@ -267,6 +303,7 @@ name="Time & Date"> <view class="android.widget.TimePicker" + topAttrs="visibility" relatedTo="DatePicker,CalendarView" render="alone" /> <view @@ -275,13 +312,16 @@ 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" @@ -291,14 +331,17 @@ 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 @@ -308,15 +351,18 @@ 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" /> @@ -328,12 +374,15 @@ 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" @@ -343,17 +392,21 @@ 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 @@ -362,9 +415,11 @@ 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" /> diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/BaseViewRuleTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/BaseViewRuleTest.java index d19e9bd..6d46b1e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/BaseViewRuleTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/BaseViewRuleTest.java @@ -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() { diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java index 3fee553..c325a40 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java @@ -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() { diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LinearLayoutRuleTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LinearLayoutRuleTest.java index 145be61..4a0fc6e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LinearLayoutRuleTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LinearLayoutRuleTest.java @@ -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)); } diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestAttributeInfo.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestAttributeInfo.java index 0f2ab21..908d0ba 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestAttributeInfo.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestAttributeInfo.java @@ -15,44 +15,57 @@ */ 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 diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java index 7d77252..ed2bc43 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java @@ -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 diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepositoryTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepositoryTest.java index 5921e85..50d438c 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepositoryTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepositoryTest.java @@ -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")); + } + } diff --git a/rule_api/src/com/android/ide/common/api/IAttributeInfo.java b/rule_api/src/com/android/ide/common/api/IAttributeInfo.java index 2a6ecd8..da1bc9e 100755 --- a/rule_api/src/com/android/ide/common/api/IAttributeInfo.java +++ b/rule_api/src/com/android/ide/common/api/IAttributeInfo.java @@ -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(); } diff --git a/rule_api/src/com/android/ide/common/api/INode.java b/rule_api/src/com/android/ide/common/api/INode.java index e3f34a9..b4cb638 100755 --- a/rule_api/src/com/android/ide/common/api/INode.java +++ b/rule_api/src/com/android/ide/common/api/INode.java @@ -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(); diff --git a/rule_api/src/com/android/ide/common/api/IViewMetadata.java b/rule_api/src/com/android/ide/common/api/IViewMetadata.java index 0687f30..8646764 100644 --- a/rule_api/src/com/android/ide/common/api/IViewMetadata.java +++ b/rule_api/src/com/android/ide/common/api/IViewMetadata.java @@ -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? |