/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/org/documents/epl-v10.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ide.eclipse.adt.internal.editors.layout.properties; import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN; import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; import com.android.annotations.Nullable; import com.android.ide.common.api.IAttributeInfo; import com.android.ide.common.api.IAttributeInfo.Format; import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import com.android.tools.lint.detector.api.LintUtils; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Link; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.browser.IWebBrowser; import org.eclipse.wb.internal.core.editor.structure.property.PropertyListIntersector; import org.eclipse.wb.internal.core.model.property.ComplexProperty; import org.eclipse.wb.internal.core.model.property.Property; import org.eclipse.wb.internal.core.model.property.category.PropertyCategory; import org.eclipse.wb.internal.core.model.property.editor.PropertyEditor; import org.eclipse.wb.internal.core.model.property.editor.presentation.ButtonPropertyEditorPresentation; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; /** * The {@link PropertyFactory} creates (and caches) the set of {@link Property} * instances applicable to a given node. It's also responsible for ordering * these, and sometimes combining them into {@link ComplexProperty} category * nodes. * <p> * TODO: For any properties that are *set* in XML, they should NOT be labeled as * advanced (which would make them disappear) */ public class PropertyFactory { /** Disable cache during development only */ @SuppressWarnings("unused") private static final boolean CACHE_ENABLED = true || !LintUtils.assertionsEnabled(); static { if (!CACHE_ENABLED) { System.err.println("WARNING: The property cache is disabled"); } } private static final Property[] NO_PROPERTIES = new Property[0]; private static final int PRIO_FIRST = -100000; private static final int PRIO_SECOND = PRIO_FIRST + 10; private static final int PRIO_LAST = 100000; private final GraphicalEditorPart mGraphicalEditorPart; private Map<UiViewElementNode, Property[]> mCache = new WeakHashMap<UiViewElementNode, Property[]>(); private UiViewElementNode mCurrentViewCookie; /** Sorting orders for the properties */ public enum SortingMode { NATURAL, BY_ORIGIN, ALPHABETICAL; } /** The default sorting mode */ public static final SortingMode DEFAULT_MODE = SortingMode.BY_ORIGIN; private SortingMode mSortMode = DEFAULT_MODE; private SortingMode mCacheSortMode; public PropertyFactory(GraphicalEditorPart graphicalEditorPart) { mGraphicalEditorPart = graphicalEditorPart; } /** * Get the properties for the given list of selection items. * * @param items the {@link CanvasViewInfo} instances to get an intersected * property list for * @return the properties for the given items */ public Property[] getProperties(List<CanvasViewInfo> items) { mCurrentViewCookie = null; if (items == null || items.size() == 0) { return NO_PROPERTIES; } else if (items.size() == 1) { CanvasViewInfo item = items.get(0); mCurrentViewCookie = item.getUiViewNode(); return getProperties(item); } else { // intersect properties PropertyListIntersector intersector = new PropertyListIntersector(); for (CanvasViewInfo node : items) { intersector.intersect(getProperties(node)); } return intersector.getProperties(); } } private Property[] getProperties(CanvasViewInfo item) { UiViewElementNode node = item.getUiViewNode(); if (node == null) { return NO_PROPERTIES; } if (mCacheSortMode != mSortMode) { mCacheSortMode = mSortMode; mCache.clear(); } Property[] properties = mCache.get(node); if (!CACHE_ENABLED) { properties = null; } if (properties == null) { Collection<? extends Property> propertyList = getProperties(node); if (propertyList == null) { properties = new Property[0]; } else { properties = propertyList.toArray(new Property[propertyList.size()]); } mCache.put(node, properties); } return properties; } protected Collection<? extends Property> getProperties(UiViewElementNode node) { ViewMetadataRepository repository = ViewMetadataRepository.get(); ViewElementDescriptor viewDescriptor = (ViewElementDescriptor) node.getDescriptor(); String fqcn = viewDescriptor.getFullClassName(); Set<String> top = new HashSet<String>(repository.getTopAttributes(fqcn)); AttributeDescriptor[] attributeDescriptors = node.getAttributeDescriptors(); List<XmlProperty> properties = new ArrayList<XmlProperty>(attributeDescriptors.length); int priority = 0; for (final AttributeDescriptor descriptor : attributeDescriptors) { // TODO: Filter out non-public properties!! // (They shouldn't be in the descriptors at all) assert !(descriptor instanceof SeparatorAttributeDescriptor); // No longer inserted if (descriptor instanceof XmlnsAttributeDescriptor) { continue; } PropertyEditor editor = XmlPropertyEditor.INSTANCE; IAttributeInfo info = descriptor.getAttributeInfo(); if (info != null) { EnumSet<Format> formats = info.getFormats(); if (formats.contains(Format.BOOLEAN)) { editor = BooleanXmlPropertyEditor.INSTANCE; } else if (formats.contains(Format.ENUM)) { // We deliberately don't use EnumXmlPropertyEditor.INSTANCE here, // since some attributes (such as layout_width) can have not just one // of the enum values but custom values such as "42dp" as well. And // furthermore, we don't even bother limiting this to formats.size()==1, // since the editing experience with the enum property editor is // more limited than the text editor plus enum completer anyway // (for example, you can't type to filter the values, and clearing // the value is harder.) } } XmlProperty property = new XmlProperty(editor, this, node, descriptor); // Assign ids sequentially. This ensures that the properties will mostly keep their // relative order (such as placing width before height), even though we will regroup // some (such as properties in the same category, and the layout params etc) priority += 10; PropertyCategory category = PropertyCategory.NORMAL; String name = descriptor.getXmlLocalName(); if (top.contains(name) || PropertyMetadata.isPreferred(name)) { category = PropertyCategory.PREFERRED; property.setPriority(PRIO_FIRST + priority); } else { property.setPriority(priority); // Prefer attributes defined on the specific type of this // widget // NOTE: This doesn't work very well for TextViews /* IAttributeInfo attributeInfo = descriptor.getAttributeInfo(); if (attributeInfo != null && fqcn.equals(attributeInfo.getDefinedBy())) { category = PropertyCategory.PREFERRED; } else*/ if (PropertyMetadata.isAdvanced(name)) { category = PropertyCategory.ADVANCED; } } if (category != null) { property.setCategory(category); } properties.add(property); } switch (mSortMode) { case BY_ORIGIN: return sortByOrigin(node, properties); case ALPHABETICAL: return sortAlphabetically(node, properties); default: case NATURAL: return sortNatural(node, properties); } } protected Collection<? extends Property> sortAlphabetically( UiViewElementNode node, List<XmlProperty> properties) { Collections.sort(properties, Property.ALPHABETICAL); return properties; } protected Collection<? extends Property> sortByOrigin( UiViewElementNode node, List<XmlProperty> properties) { List<Property> collapsed = new ArrayList<Property>(properties.size()); List<Property> layoutProperties = Lists.newArrayListWithExpectedSize(20); List<Property> marginProperties = null; List<Property> deprecatedProperties = null; Map<String, ComplexProperty> categoryToProperty = new HashMap<String, ComplexProperty>(); Multimap<String, Property> categoryToProperties = ArrayListMultimap.create(); if (properties.isEmpty()) { return properties; } ViewElementDescriptor parent = (ViewElementDescriptor) properties.get(0).getDescriptor() .getParent(); Map<String, Integer> categoryPriorities = Maps.newHashMap(); int nextCategoryPriority = 100; while (parent != null) { categoryPriorities.put(parent.getFullClassName(), nextCategoryPriority += 100); parent = parent.getSuperClassDesc(); } for (int i = 0, max = properties.size(); i < max; i++) { XmlProperty property = properties.get(i); AttributeDescriptor descriptor = property.getDescriptor(); if (descriptor.isDeprecated()) { if (deprecatedProperties == null) { deprecatedProperties = Lists.newArrayListWithExpectedSize(10); } deprecatedProperties.add(property); continue; } String firstName = descriptor.getXmlLocalName(); if (firstName.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) { if (firstName.startsWith(ATTR_LAYOUT_MARGIN)) { if (marginProperties == null) { marginProperties = Lists.newArrayListWithExpectedSize(5); } marginProperties.add(property); } else { layoutProperties.add(property); } continue; } if (firstName.equals(ATTR_ID)) { // Add id to the front (though the layout parameters will be added to // the front of this at the end) property.setPriority(PRIO_FIRST); collapsed.add(property); continue; } if (property.getCategory() == PropertyCategory.PREFERRED) { collapsed.add(property); // Fall through: these are *duplicated* inside their defining categories! // However, create a new instance of the property, such that the propertysheet // doesn't see the same property instance twice (when selected, it will highlight // both, etc.) Also, set the category to Normal such that we don't draw attention // to it again. We want it to appear in both places such that somebody looking // within a category will always find it there, even if for this specific // view type it's a common attribute and replicated up at the top. XmlProperty oldProperty = property; property = new XmlProperty(oldProperty.getEditor(), this, node, oldProperty.getDescriptor()); property.setPriority(oldProperty.getPriority()); } IAttributeInfo attributeInfo = descriptor.getAttributeInfo(); if (attributeInfo != null && attributeInfo.getDefinedBy() != null) { String category = attributeInfo.getDefinedBy(); ComplexProperty complex = categoryToProperty.get(category); if (complex == null) { complex = new ComplexProperty( category.substring(category.lastIndexOf('.') + 1), "[]", null /* properties */); categoryToProperty.put(category, complex); Integer categoryPriority = categoryPriorities.get(category); if (categoryPriority != null) { complex.setPriority(categoryPriority); } else { // Descriptor for an attribute whose definedBy does *not* // correspond to one of the known superclasses of this widget. // This sometimes happens; for example, a RatingBar will pull in // an ImageView's minWidth attribute. Probably an error in the // metadata, but deal with it gracefully here. categoryPriorities.put(category, nextCategoryPriority += 100); complex.setPriority(nextCategoryPriority); } } categoryToProperties.put(category, property); continue; } else { collapsed.add(property); } } // Update the complex properties for (String category : categoryToProperties.keySet()) { Collection<Property> subProperties = categoryToProperties.get(category); if (subProperties.size() > 1) { ComplexProperty complex = categoryToProperty.get(category); assert complex != null : category; Property[] subArray = new Property[subProperties.size()]; complex.setProperties(subProperties.toArray(subArray)); //complex.setPriority(subArray[0].getPriority()); collapsed.add(complex); boolean allAdvanced = true; boolean isPreferred = false; for (Property p : subProperties) { PropertyCategory c = p.getCategory(); if (c != PropertyCategory.ADVANCED) { allAdvanced = false; } if (c == PropertyCategory.PREFERRED) { isPreferred = true; } } if (isPreferred) { complex.setCategory(PropertyCategory.PREFERRED); } else if (allAdvanced) { complex.setCategory(PropertyCategory.ADVANCED); } } else if (subProperties.size() == 1) { collapsed.add(subProperties.iterator().next()); } } if (layoutProperties.size() > 0 || marginProperties != null) { if (marginProperties != null) { XmlProperty[] m = marginProperties.toArray(new XmlProperty[marginProperties.size()]); Property marginProperty = new ComplexProperty( "Margins", "[]", m); layoutProperties.add(marginProperty); marginProperty.setPriority(PRIO_LAST); for (XmlProperty p : m) { p.setParent(marginProperty); } } Property[] l = layoutProperties.toArray(new Property[layoutProperties.size()]); Arrays.sort(l, Property.PRIORITY); Property property = new ComplexProperty( "Layout Parameters", "[]", l); for (Property p : l) { if (p instanceof XmlProperty) { ((XmlProperty) p).setParent(property); } } property.setCategory(PropertyCategory.PREFERRED); collapsed.add(property); property.setPriority(PRIO_SECOND); } if (deprecatedProperties != null && deprecatedProperties.size() > 0) { Property property = new ComplexProperty( "Deprecated", "(Deprecated Properties)", deprecatedProperties.toArray(new Property[deprecatedProperties.size()])); property.setPriority(PRIO_LAST); collapsed.add(property); } Collections.sort(collapsed, Property.PRIORITY); return collapsed; } protected Collection<? extends Property> sortNatural( UiViewElementNode node, List<XmlProperty> properties) { Collections.sort(properties, Property.ALPHABETICAL); List<Property> collapsed = new ArrayList<Property>(properties.size()); List<Property> layoutProperties = Lists.newArrayListWithExpectedSize(20); List<Property> marginProperties = null; List<Property> deprecatedProperties = null; Map<String, ComplexProperty> categoryToProperty = new HashMap<String, ComplexProperty>(); Multimap<String, Property> categoryToProperties = ArrayListMultimap.create(); for (int i = 0, max = properties.size(); i < max; i++) { XmlProperty property = properties.get(i); AttributeDescriptor descriptor = property.getDescriptor(); if (descriptor.isDeprecated()) { if (deprecatedProperties == null) { deprecatedProperties = Lists.newArrayListWithExpectedSize(10); } deprecatedProperties.add(property); continue; } String firstName = descriptor.getXmlLocalName(); if (firstName.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) { if (firstName.startsWith(ATTR_LAYOUT_MARGIN)) { if (marginProperties == null) { marginProperties = Lists.newArrayListWithExpectedSize(5); } marginProperties.add(property); } else { layoutProperties.add(property); } continue; } if (firstName.equals(ATTR_ID)) { // Add id to the front (though the layout parameters will be added to // the front of this at the end) property.setPriority(PRIO_FIRST); collapsed.add(property); continue; } String category = PropertyMetadata.getCategory(firstName); if (category != null) { ComplexProperty complex = categoryToProperty.get(category); if (complex == null) { complex = new ComplexProperty( category, "[]", null /* properties */); categoryToProperty.put(category, complex); complex.setPriority(property.getPriority()); } categoryToProperties.put(category, property); continue; } // Index of second word in the first name, so in fooBar it's 3 (index of 'B') int firstNameIndex = firstName.length(); for (int k = 0, kn = firstName.length(); k < kn; k++) { if (Character.isUpperCase(firstName.charAt(k))) { firstNameIndex = k; break; } } // Scout forwards and see how many properties we can combine int j = i + 1; if (property.getCategory() != PropertyCategory.PREFERRED && !property.getDescriptor().isDeprecated()) { for (; j < max; j++) { XmlProperty next = properties.get(j); String nextName = next.getName(); if (nextName.regionMatches(0, firstName, 0, firstNameIndex) // Also make sure we begin the second word at the next // character; if not, we could have something like // scrollBar // scrollingBehavior && nextName.length() > firstNameIndex && Character.isUpperCase(nextName.charAt(firstNameIndex))) { // Deprecated attributes, and preferred attributes, should not // be pushed into normal clusters (preferred stay top-level // and sort to the top, deprecated are all put in the same cluster at // the end) if (next.getCategory() == PropertyCategory.PREFERRED) { break; } if (next.getDescriptor().isDeprecated()) { break; } // This property should be combined with the previous // property } else { break; } } } if (j - i > 1) { // Combining multiple properties: all the properties from i // through j inclusive XmlProperty[] subprops = new XmlProperty[j - i]; for (int k = i, index = 0; k < j; k++, index++) { subprops[index] = properties.get(k); } Arrays.sort(subprops, Property.PRIORITY); // See if we can compute a LONGER base than just the first word. // For example, if we have "lineSpacingExtra" and "lineSpacingMultiplier" // we'd like the base to be "lineSpacing", not "line". int common = firstNameIndex; for (int k = firstNameIndex + 1, n = firstName.length(); k < n; k++) { if (Character.isUpperCase(firstName.charAt(k))) { common = k; break; } } if (common > firstNameIndex) { for (int k = 0, n = subprops.length; k < n; k++) { String nextName = subprops[k].getName(); if (nextName.regionMatches(0, firstName, 0, common) // Also make sure we begin the second word at the next // character; if not, we could have something like // scrollBar // scrollingBehavior && nextName.length() > common && Character.isUpperCase(nextName.charAt(common))) { // New prefix is okay } else { common = firstNameIndex; break; } } firstNameIndex = common; } String base = firstName.substring(0, firstNameIndex); base = DescriptorsUtils.capitalize(base); Property complexProperty = new ComplexProperty( base, "[]", subprops); complexProperty.setPriority(subprops[0].getPriority()); //complexProperty.setCategory(PropertyCategory.PREFERRED); collapsed.add(complexProperty); boolean allAdvanced = true; boolean isPreferred = false; for (XmlProperty p : subprops) { p.setParent(complexProperty); PropertyCategory c = p.getCategory(); if (c != PropertyCategory.ADVANCED) { allAdvanced = false; } if (c == PropertyCategory.PREFERRED) { isPreferred = true; } } if (isPreferred) { complexProperty.setCategory(PropertyCategory.PREFERRED); } else if (allAdvanced) { complexProperty.setCategory(PropertyCategory.PREFERRED); } } else { // Add the individual properties (usually 1, sometimes 2 for (int k = i; k < j; k++) { collapsed.add(properties.get(k)); } } i = j - 1; // -1: compensate in advance for the for-loop adding 1 } // Update the complex properties for (String category : categoryToProperties.keySet()) { Collection<Property> subProperties = categoryToProperties.get(category); if (subProperties.size() > 1) { ComplexProperty complex = categoryToProperty.get(category); assert complex != null : category; Property[] subArray = new Property[subProperties.size()]; complex.setProperties(subProperties.toArray(subArray)); complex.setPriority(subArray[0].getPriority()); collapsed.add(complex); boolean allAdvanced = true; boolean isPreferred = false; for (Property p : subProperties) { PropertyCategory c = p.getCategory(); if (c != PropertyCategory.ADVANCED) { allAdvanced = false; } if (c == PropertyCategory.PREFERRED) { isPreferred = true; } } if (isPreferred) { complex.setCategory(PropertyCategory.PREFERRED); } else if (allAdvanced) { complex.setCategory(PropertyCategory.ADVANCED); } } else if (subProperties.size() == 1) { collapsed.add(subProperties.iterator().next()); } } if (layoutProperties.size() > 0 || marginProperties != null) { if (marginProperties != null) { XmlProperty[] m = marginProperties.toArray(new XmlProperty[marginProperties.size()]); Property marginProperty = new ComplexProperty( "Margins", "[]", m); layoutProperties.add(marginProperty); marginProperty.setPriority(PRIO_LAST); for (XmlProperty p : m) { p.setParent(marginProperty); } } Property[] l = layoutProperties.toArray(new Property[layoutProperties.size()]); Arrays.sort(l, Property.PRIORITY); Property property = new ComplexProperty( "Layout Parameters", "[]", l); for (Property p : l) { if (p instanceof XmlProperty) { ((XmlProperty) p).setParent(property); } } property.setCategory(PropertyCategory.PREFERRED); collapsed.add(property); property.setPriority(PRIO_SECOND); } if (deprecatedProperties != null && deprecatedProperties.size() > 0) { Property property = new ComplexProperty( "Deprecated", "(Deprecated Properties)", deprecatedProperties.toArray(new Property[deprecatedProperties.size()])); property.setPriority(PRIO_LAST); collapsed.add(property); } Collections.sort(collapsed, Property.PRIORITY); return collapsed; } @Nullable GraphicalEditorPart getGraphicalEditor() { return mGraphicalEditorPart; } // HACK: This should be passed into each property instead public Object getCurrentViewObject() { return mCurrentViewCookie; } public void setSortingMode(SortingMode sortingMode) { mSortMode = sortingMode; } // https://bugs.eclipse.org/bugs/show_bug.cgi?id=388574 public static Composite addWorkaround(Composite parent) { if (ButtonPropertyEditorPresentation.isInWorkaround) { Composite top = new Composite(parent, SWT.NONE); top.setLayout(new GridLayout(1, false)); Label label = new Label(top, SWT.WRAP); label.setText( "This dialog is shown instead of an inline text editor as a\n" + "workaround for an Eclipse bug specific to OSX Mountain Lion.\n" + "It should be fixed in Eclipse 4.3."); label.setForeground(top.getDisplay().getSystemColor(SWT.COLOR_RED)); GridData data = new GridData(); data.grabExcessVerticalSpace = false; data.grabExcessHorizontalSpace = false; data.horizontalAlignment = GridData.FILL; data.verticalAlignment = GridData.BEGINNING; label.setLayoutData(data); Link link = new Link(top, SWT.NO_FOCUS); link.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1)); link.setText("<a>https://bugs.eclipse.org/bugs/show_bug.cgi?id=388574</a>"); link.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { try { IWorkbench workbench = PlatformUI.getWorkbench(); IWebBrowser browser = workbench.getBrowserSupport().getExternalBrowser(); browser.openURL(new URL(event.text)); } catch (Exception e) { String message = String.format( "Could not open browser. Vist\n%1$s\ninstead.", event.text); MessageDialog.openError(((Link)event.getSource()).getShell(), "Browser Error", message); } } }); return top; } return null; } }