1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.ide.eclipse.adt.internal.editors.layout.properties;
17 
18 import static com.android.SdkConstants.ATTR_ID;
19 import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
20 import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
21 
22 import com.android.annotations.Nullable;
23 import com.android.ide.common.api.IAttributeInfo;
24 import com.android.ide.common.api.IAttributeInfo.Format;
25 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
26 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
27 import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
28 import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
29 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
30 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
31 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
32 import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
33 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
34 import com.android.tools.lint.detector.api.LintUtils;
35 import com.google.common.collect.ArrayListMultimap;
36 import com.google.common.collect.Lists;
37 import com.google.common.collect.Maps;
38 import com.google.common.collect.Multimap;
39 
40 import org.eclipse.jface.dialogs.MessageDialog;
41 import org.eclipse.swt.SWT;
42 import org.eclipse.swt.events.SelectionAdapter;
43 import org.eclipse.swt.events.SelectionEvent;
44 import org.eclipse.swt.layout.GridData;
45 import org.eclipse.swt.layout.GridLayout;
46 import org.eclipse.swt.widgets.Composite;
47 import org.eclipse.swt.widgets.Label;
48 import org.eclipse.swt.widgets.Link;
49 import org.eclipse.ui.IWorkbench;
50 import org.eclipse.ui.PlatformUI;
51 import org.eclipse.ui.browser.IWebBrowser;
52 import org.eclipse.wb.internal.core.editor.structure.property.PropertyListIntersector;
53 import org.eclipse.wb.internal.core.model.property.ComplexProperty;
54 import org.eclipse.wb.internal.core.model.property.Property;
55 import org.eclipse.wb.internal.core.model.property.category.PropertyCategory;
56 import org.eclipse.wb.internal.core.model.property.editor.PropertyEditor;
57 import org.eclipse.wb.internal.core.model.property.editor.presentation.ButtonPropertyEditorPresentation;
58 
59 import java.net.URL;
60 import java.util.ArrayList;
61 import java.util.Arrays;
62 import java.util.Collection;
63 import java.util.Collections;
64 import java.util.EnumSet;
65 import java.util.HashMap;
66 import java.util.HashSet;
67 import java.util.List;
68 import java.util.Map;
69 import java.util.Set;
70 import java.util.WeakHashMap;
71 
72 /**
73  * The {@link PropertyFactory} creates (and caches) the set of {@link Property}
74  * instances applicable to a given node. It's also responsible for ordering
75  * these, and sometimes combining them into {@link ComplexProperty} category
76  * nodes.
77  * <p>
78  * TODO: For any properties that are *set* in XML, they should NOT be labeled as
79  * advanced (which would make them disappear)
80  */
81 public class PropertyFactory {
82     /** Disable cache during development only */
83     @SuppressWarnings("unused")
84     private static final boolean CACHE_ENABLED = true || !LintUtils.assertionsEnabled();
85     static {
86         if (!CACHE_ENABLED) {
87             System.err.println("WARNING: The property cache is disabled");
88         }
89     }
90 
91     private static final Property[] NO_PROPERTIES = new Property[0];
92 
93     private static final int PRIO_FIRST = -100000;
94     private static final int PRIO_SECOND = PRIO_FIRST + 10;
95     private static final int PRIO_LAST = 100000;
96 
97     private final GraphicalEditorPart mGraphicalEditorPart;
98     private Map<UiViewElementNode, Property[]> mCache =
99             new WeakHashMap<UiViewElementNode, Property[]>();
100     private UiViewElementNode mCurrentViewCookie;
101 
102     /** Sorting orders for the properties */
103     public enum SortingMode {
104         NATURAL,
105         BY_ORIGIN,
106         ALPHABETICAL;
107     }
108 
109     /** The default sorting mode */
110     public static final SortingMode DEFAULT_MODE = SortingMode.BY_ORIGIN;
111 
112     private SortingMode mSortMode = DEFAULT_MODE;
113     private SortingMode mCacheSortMode;
114 
PropertyFactory(GraphicalEditorPart graphicalEditorPart)115     public PropertyFactory(GraphicalEditorPart graphicalEditorPart) {
116         mGraphicalEditorPart = graphicalEditorPart;
117     }
118 
119     /**
120      * Get the properties for the given list of selection items.
121      *
122      * @param items the {@link CanvasViewInfo} instances to get an intersected
123      *            property list for
124      * @return the properties for the given items
125      */
getProperties(List<CanvasViewInfo> items)126     public Property[] getProperties(List<CanvasViewInfo> items) {
127         mCurrentViewCookie = null;
128 
129         if (items == null || items.size() == 0) {
130             return NO_PROPERTIES;
131         } else if (items.size() == 1) {
132             CanvasViewInfo item = items.get(0);
133             mCurrentViewCookie = item.getUiViewNode();
134 
135             return getProperties(item);
136         } else {
137             // intersect properties
138             PropertyListIntersector intersector = new PropertyListIntersector();
139             for (CanvasViewInfo node : items) {
140                 intersector.intersect(getProperties(node));
141             }
142 
143             return intersector.getProperties();
144         }
145     }
146 
getProperties(CanvasViewInfo item)147     private Property[] getProperties(CanvasViewInfo item) {
148         UiViewElementNode node = item.getUiViewNode();
149         if (node == null) {
150             return NO_PROPERTIES;
151         }
152 
153         if (mCacheSortMode != mSortMode) {
154             mCacheSortMode = mSortMode;
155             mCache.clear();
156         }
157 
158         Property[] properties = mCache.get(node);
159         if (!CACHE_ENABLED) {
160             properties = null;
161         }
162         if (properties == null) {
163             Collection<? extends Property> propertyList = getProperties(node);
164             if (propertyList == null) {
165                 properties = new Property[0];
166             } else {
167                 properties = propertyList.toArray(new Property[propertyList.size()]);
168             }
169             mCache.put(node, properties);
170         }
171         return properties;
172     }
173 
174 
getProperties(UiViewElementNode node)175     protected Collection<? extends Property> getProperties(UiViewElementNode node) {
176         ViewMetadataRepository repository = ViewMetadataRepository.get();
177         ViewElementDescriptor viewDescriptor = (ViewElementDescriptor) node.getDescriptor();
178         String fqcn = viewDescriptor.getFullClassName();
179         Set<String> top = new HashSet<String>(repository.getTopAttributes(fqcn));
180         AttributeDescriptor[] attributeDescriptors = node.getAttributeDescriptors();
181 
182         List<XmlProperty> properties = new ArrayList<XmlProperty>(attributeDescriptors.length);
183         int priority = 0;
184         for (final AttributeDescriptor descriptor : attributeDescriptors) {
185             // TODO: Filter out non-public properties!!
186             // (They shouldn't be in the descriptors at all)
187 
188             assert !(descriptor instanceof SeparatorAttributeDescriptor); // No longer inserted
189             if (descriptor instanceof XmlnsAttributeDescriptor) {
190                 continue;
191             }
192 
193             PropertyEditor editor = XmlPropertyEditor.INSTANCE;
194             IAttributeInfo info = descriptor.getAttributeInfo();
195             if (info != null) {
196                 EnumSet<Format> formats = info.getFormats();
197                 if (formats.contains(Format.BOOLEAN)) {
198                     editor = BooleanXmlPropertyEditor.INSTANCE;
199                 } else if (formats.contains(Format.ENUM)) {
200                     // We deliberately don't use EnumXmlPropertyEditor.INSTANCE here,
201                     // since some attributes (such as layout_width) can have not just one
202                     // of the enum values but custom values such as "42dp" as well. And
203                     // furthermore, we don't even bother limiting this to formats.size()==1,
204                     // since the editing experience with the enum property editor is
205                     // more limited than the text editor plus enum completer anyway
206                     // (for example, you can't type to filter the values, and clearing
207                     // the value is harder.)
208                 }
209             }
210 
211             XmlProperty property = new XmlProperty(editor, this, node, descriptor);
212             // Assign ids sequentially. This ensures that the properties will mostly keep their
213             // relative order (such as placing width before height), even though we will regroup
214             // some (such as properties in the same category, and the layout params etc)
215             priority += 10;
216 
217             PropertyCategory category = PropertyCategory.NORMAL;
218             String name = descriptor.getXmlLocalName();
219             if (top.contains(name) || PropertyMetadata.isPreferred(name)) {
220                 category = PropertyCategory.PREFERRED;
221                 property.setPriority(PRIO_FIRST + priority);
222             } else {
223                 property.setPriority(priority);
224 
225                 // Prefer attributes defined on the specific type of this
226                 // widget
227                 // NOTE: This doesn't work very well for TextViews
228                /* IAttributeInfo attributeInfo = descriptor.getAttributeInfo();
229                 if (attributeInfo != null && fqcn.equals(attributeInfo.getDefinedBy())) {
230                     category = PropertyCategory.PREFERRED;
231                 } else*/ if (PropertyMetadata.isAdvanced(name)) {
232                     category = PropertyCategory.ADVANCED;
233                 }
234             }
235             if (category != null) {
236                 property.setCategory(category);
237             }
238             properties.add(property);
239         }
240 
241         switch (mSortMode) {
242             case BY_ORIGIN:
243                 return sortByOrigin(node, properties);
244 
245             case ALPHABETICAL:
246                 return sortAlphabetically(node, properties);
247 
248             default:
249             case NATURAL:
250                 return sortNatural(node, properties);
251         }
252     }
253 
sortAlphabetically( UiViewElementNode node, List<XmlProperty> properties)254     protected Collection<? extends Property> sortAlphabetically(
255             UiViewElementNode node,
256             List<XmlProperty> properties) {
257         Collections.sort(properties, Property.ALPHABETICAL);
258         return properties;
259     }
260 
sortByOrigin( UiViewElementNode node, List<XmlProperty> properties)261     protected Collection<? extends Property> sortByOrigin(
262             UiViewElementNode node,
263             List<XmlProperty> properties) {
264         List<Property> collapsed = new ArrayList<Property>(properties.size());
265         List<Property> layoutProperties = Lists.newArrayListWithExpectedSize(20);
266         List<Property> marginProperties = null;
267         List<Property> deprecatedProperties = null;
268         Map<String, ComplexProperty> categoryToProperty = new HashMap<String, ComplexProperty>();
269         Multimap<String, Property> categoryToProperties = ArrayListMultimap.create();
270 
271         if (properties.isEmpty()) {
272             return properties;
273         }
274 
275         ViewElementDescriptor parent = (ViewElementDescriptor) properties.get(0).getDescriptor()
276                 .getParent();
277         Map<String, Integer> categoryPriorities = Maps.newHashMap();
278         int nextCategoryPriority = 100;
279         while (parent != null) {
280             categoryPriorities.put(parent.getFullClassName(), nextCategoryPriority += 100);
281             parent = parent.getSuperClassDesc();
282         }
283 
284         for (int i = 0, max = properties.size(); i < max; i++) {
285             XmlProperty property = properties.get(i);
286 
287             AttributeDescriptor descriptor = property.getDescriptor();
288             if (descriptor.isDeprecated()) {
289                 if (deprecatedProperties == null) {
290                     deprecatedProperties = Lists.newArrayListWithExpectedSize(10);
291                 }
292                 deprecatedProperties.add(property);
293                 continue;
294             }
295 
296             String firstName = descriptor.getXmlLocalName();
297             if (firstName.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
298                 if (firstName.startsWith(ATTR_LAYOUT_MARGIN)) {
299                     if (marginProperties == null) {
300                         marginProperties = Lists.newArrayListWithExpectedSize(5);
301                     }
302                     marginProperties.add(property);
303                 } else {
304                     layoutProperties.add(property);
305                 }
306                 continue;
307             }
308 
309             if (firstName.equals(ATTR_ID)) {
310                 // Add id to the front (though the layout parameters will be added to
311                 // the front of this at the end)
312                 property.setPriority(PRIO_FIRST);
313                 collapsed.add(property);
314                 continue;
315             }
316 
317             if (property.getCategory() == PropertyCategory.PREFERRED) {
318                 collapsed.add(property);
319                 // Fall through: these are *duplicated* inside their defining categories!
320                 // However, create a new instance of the property, such that the propertysheet
321                 // doesn't see the same property instance twice (when selected, it will highlight
322                 // both, etc.) Also, set the category to Normal such that we don't draw attention
323                 // to it again. We want it to appear in both places such that somebody looking
324                 // within a category will always find it there, even if for this specific
325                 // view type it's a common attribute and replicated up at the top.
326                 XmlProperty oldProperty = property;
327                 property = new XmlProperty(oldProperty.getEditor(), this, node,
328                         oldProperty.getDescriptor());
329                 property.setPriority(oldProperty.getPriority());
330             }
331 
332             IAttributeInfo attributeInfo = descriptor.getAttributeInfo();
333             if (attributeInfo != null && attributeInfo.getDefinedBy() != null) {
334                 String category = attributeInfo.getDefinedBy();
335                 ComplexProperty complex = categoryToProperty.get(category);
336                 if (complex == null) {
337                     complex = new ComplexProperty(
338                             category.substring(category.lastIndexOf('.') + 1),
339                             "[]",
340                             null /* properties */);
341                     categoryToProperty.put(category, complex);
342                     Integer categoryPriority = categoryPriorities.get(category);
343                     if (categoryPriority != null) {
344                         complex.setPriority(categoryPriority);
345                     } else {
346                         // Descriptor for an attribute whose definedBy does *not*
347                         // correspond to one of the known superclasses of this widget.
348                         // This sometimes happens; for example, a RatingBar will pull in
349                         // an ImageView's minWidth attribute. Probably an error in the
350                         // metadata, but deal with it gracefully here.
351                         categoryPriorities.put(category, nextCategoryPriority += 100);
352                         complex.setPriority(nextCategoryPriority);
353                     }
354                 }
355                 categoryToProperties.put(category, property);
356                 continue;
357             } else {
358                 collapsed.add(property);
359             }
360         }
361 
362         // Update the complex properties
363         for (String category : categoryToProperties.keySet()) {
364             Collection<Property> subProperties = categoryToProperties.get(category);
365             if (subProperties.size() > 1) {
366                 ComplexProperty complex = categoryToProperty.get(category);
367                 assert complex != null : category;
368                 Property[] subArray = new Property[subProperties.size()];
369                 complex.setProperties(subProperties.toArray(subArray));
370                 //complex.setPriority(subArray[0].getPriority());
371 
372                 collapsed.add(complex);
373 
374                 boolean allAdvanced = true;
375                 boolean isPreferred = false;
376                 for (Property p : subProperties) {
377                     PropertyCategory c = p.getCategory();
378                     if (c != PropertyCategory.ADVANCED) {
379                         allAdvanced = false;
380                     }
381                     if (c == PropertyCategory.PREFERRED) {
382                         isPreferred = true;
383                     }
384                 }
385                 if (isPreferred) {
386                     complex.setCategory(PropertyCategory.PREFERRED);
387                 } else if (allAdvanced) {
388                     complex.setCategory(PropertyCategory.ADVANCED);
389                 }
390             } else if (subProperties.size() == 1) {
391                 collapsed.add(subProperties.iterator().next());
392             }
393         }
394 
395         if (layoutProperties.size() > 0 || marginProperties != null) {
396             if (marginProperties != null) {
397                 XmlProperty[] m =
398                         marginProperties.toArray(new XmlProperty[marginProperties.size()]);
399                 Property marginProperty = new ComplexProperty(
400                         "Margins",
401                         "[]",
402                         m);
403                 layoutProperties.add(marginProperty);
404                 marginProperty.setPriority(PRIO_LAST);
405 
406                 for (XmlProperty p : m) {
407                     p.setParent(marginProperty);
408                 }
409             }
410             Property[] l = layoutProperties.toArray(new Property[layoutProperties.size()]);
411             Arrays.sort(l, Property.PRIORITY);
412             Property property = new ComplexProperty(
413                     "Layout Parameters",
414                     "[]",
415                     l);
416             for (Property p : l) {
417                 if (p instanceof XmlProperty) {
418                     ((XmlProperty) p).setParent(property);
419                 }
420             }
421             property.setCategory(PropertyCategory.PREFERRED);
422             collapsed.add(property);
423             property.setPriority(PRIO_SECOND);
424         }
425 
426         if (deprecatedProperties != null && deprecatedProperties.size() > 0) {
427             Property property = new ComplexProperty(
428                     "Deprecated",
429                     "(Deprecated Properties)",
430                     deprecatedProperties.toArray(new Property[deprecatedProperties.size()]));
431             property.setPriority(PRIO_LAST);
432             collapsed.add(property);
433         }
434 
435         Collections.sort(collapsed, Property.PRIORITY);
436 
437         return collapsed;
438     }
439 
sortNatural( UiViewElementNode node, List<XmlProperty> properties)440     protected Collection<? extends Property> sortNatural(
441             UiViewElementNode node,
442             List<XmlProperty> properties) {
443         Collections.sort(properties, Property.ALPHABETICAL);
444         List<Property> collapsed = new ArrayList<Property>(properties.size());
445         List<Property> layoutProperties = Lists.newArrayListWithExpectedSize(20);
446         List<Property> marginProperties = null;
447         List<Property> deprecatedProperties = null;
448         Map<String, ComplexProperty> categoryToProperty = new HashMap<String, ComplexProperty>();
449         Multimap<String, Property> categoryToProperties = ArrayListMultimap.create();
450 
451         for (int i = 0, max = properties.size(); i < max; i++) {
452             XmlProperty property = properties.get(i);
453 
454             AttributeDescriptor descriptor = property.getDescriptor();
455             if (descriptor.isDeprecated()) {
456                 if (deprecatedProperties == null) {
457                     deprecatedProperties = Lists.newArrayListWithExpectedSize(10);
458                 }
459                 deprecatedProperties.add(property);
460                 continue;
461             }
462 
463             String firstName = descriptor.getXmlLocalName();
464             if (firstName.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
465                 if (firstName.startsWith(ATTR_LAYOUT_MARGIN)) {
466                     if (marginProperties == null) {
467                         marginProperties = Lists.newArrayListWithExpectedSize(5);
468                     }
469                     marginProperties.add(property);
470                 } else {
471                     layoutProperties.add(property);
472                 }
473                 continue;
474             }
475 
476             if (firstName.equals(ATTR_ID)) {
477                 // Add id to the front (though the layout parameters will be added to
478                 // the front of this at the end)
479                 property.setPriority(PRIO_FIRST);
480                 collapsed.add(property);
481                 continue;
482             }
483 
484             String category = PropertyMetadata.getCategory(firstName);
485             if (category != null) {
486                 ComplexProperty complex = categoryToProperty.get(category);
487                 if (complex == null) {
488                     complex = new ComplexProperty(
489                             category,
490                             "[]",
491                             null /* properties */);
492                     categoryToProperty.put(category, complex);
493                     complex.setPriority(property.getPriority());
494                 }
495                 categoryToProperties.put(category, property);
496                 continue;
497             }
498 
499             // Index of second word in the first name, so in fooBar it's 3 (index of 'B')
500             int firstNameIndex = firstName.length();
501             for (int k = 0, kn = firstName.length(); k < kn; k++) {
502                 if (Character.isUpperCase(firstName.charAt(k))) {
503                     firstNameIndex = k;
504                     break;
505                 }
506             }
507 
508             // Scout forwards and see how many properties we can combine
509             int j = i + 1;
510             if (property.getCategory() != PropertyCategory.PREFERRED
511                     && !property.getDescriptor().isDeprecated()) {
512                 for (; j < max; j++) {
513                     XmlProperty next = properties.get(j);
514                     String nextName = next.getName();
515                     if (nextName.regionMatches(0, firstName, 0, firstNameIndex)
516                             // Also make sure we begin the second word at the next
517                             // character; if not, we could have something like
518                             // scrollBar
519                             // scrollingBehavior
520                             && nextName.length() > firstNameIndex
521                             && Character.isUpperCase(nextName.charAt(firstNameIndex))) {
522 
523                         // Deprecated attributes, and preferred attributes, should not
524                         // be pushed into normal clusters (preferred stay top-level
525                         // and sort to the top, deprecated are all put in the same cluster at
526                         // the end)
527 
528                         if (next.getCategory() == PropertyCategory.PREFERRED) {
529                             break;
530                         }
531                         if (next.getDescriptor().isDeprecated()) {
532                             break;
533                         }
534 
535                         // This property should be combined with the previous
536                         // property
537                     } else {
538                         break;
539                     }
540                 }
541             }
542             if (j - i > 1) {
543                 // Combining multiple properties: all the properties from i
544                 // through j inclusive
545                 XmlProperty[] subprops = new XmlProperty[j - i];
546                 for (int k = i, index = 0; k < j; k++, index++) {
547                     subprops[index] = properties.get(k);
548                 }
549                 Arrays.sort(subprops, Property.PRIORITY);
550 
551                 // See if we can compute a LONGER base than just the first word.
552                 // For example, if we have "lineSpacingExtra" and "lineSpacingMultiplier"
553                 // we'd like the base to be "lineSpacing", not "line".
554                 int common = firstNameIndex;
555                 for (int k = firstNameIndex + 1, n = firstName.length(); k < n; k++) {
556                     if (Character.isUpperCase(firstName.charAt(k))) {
557                         common = k;
558                         break;
559                     }
560                 }
561                 if (common > firstNameIndex) {
562                     for (int k = 0, n = subprops.length; k < n; k++) {
563                         String nextName = subprops[k].getName();
564                         if (nextName.regionMatches(0, firstName, 0, common)
565                                 // Also make sure we begin the second word at the next
566                                 // character; if not, we could have something like
567                                 // scrollBar
568                                 // scrollingBehavior
569                                 && nextName.length() > common
570                                 && Character.isUpperCase(nextName.charAt(common))) {
571                             // New prefix is okay
572                         } else {
573                             common = firstNameIndex;
574                             break;
575                         }
576                     }
577                     firstNameIndex = common;
578                 }
579 
580                 String base = firstName.substring(0, firstNameIndex);
581                 base = DescriptorsUtils.capitalize(base);
582                 Property complexProperty = new ComplexProperty(
583                         base,
584                         "[]",
585                         subprops);
586                 complexProperty.setPriority(subprops[0].getPriority());
587                 //complexProperty.setCategory(PropertyCategory.PREFERRED);
588                 collapsed.add(complexProperty);
589                 boolean allAdvanced = true;
590                 boolean isPreferred = false;
591                 for (XmlProperty p : subprops) {
592                     p.setParent(complexProperty);
593                     PropertyCategory c = p.getCategory();
594                     if (c != PropertyCategory.ADVANCED) {
595                         allAdvanced = false;
596                     }
597                     if (c == PropertyCategory.PREFERRED) {
598                         isPreferred = true;
599                     }
600                 }
601                 if (isPreferred) {
602                     complexProperty.setCategory(PropertyCategory.PREFERRED);
603                 } else if (allAdvanced) {
604                     complexProperty.setCategory(PropertyCategory.PREFERRED);
605                 }
606             } else {
607                 // Add the individual properties (usually 1, sometimes 2
608                 for (int k = i; k < j; k++) {
609                     collapsed.add(properties.get(k));
610                 }
611             }
612 
613             i = j - 1; // -1: compensate in advance for the for-loop adding 1
614         }
615 
616         // Update the complex properties
617         for (String category : categoryToProperties.keySet()) {
618             Collection<Property> subProperties = categoryToProperties.get(category);
619             if (subProperties.size() > 1) {
620                 ComplexProperty complex = categoryToProperty.get(category);
621                 assert complex != null : category;
622                 Property[] subArray = new Property[subProperties.size()];
623                 complex.setProperties(subProperties.toArray(subArray));
624                 complex.setPriority(subArray[0].getPriority());
625                 collapsed.add(complex);
626 
627                 boolean allAdvanced = true;
628                 boolean isPreferred = false;
629                 for (Property p : subProperties) {
630                     PropertyCategory c = p.getCategory();
631                     if (c != PropertyCategory.ADVANCED) {
632                         allAdvanced = false;
633                     }
634                     if (c == PropertyCategory.PREFERRED) {
635                         isPreferred = true;
636                     }
637                 }
638                 if (isPreferred) {
639                     complex.setCategory(PropertyCategory.PREFERRED);
640                 } else if (allAdvanced) {
641                     complex.setCategory(PropertyCategory.ADVANCED);
642                 }
643             } else if (subProperties.size() == 1) {
644                 collapsed.add(subProperties.iterator().next());
645             }
646         }
647 
648         if (layoutProperties.size() > 0 || marginProperties != null) {
649             if (marginProperties != null) {
650                 XmlProperty[] m =
651                         marginProperties.toArray(new XmlProperty[marginProperties.size()]);
652                 Property marginProperty = new ComplexProperty(
653                         "Margins",
654                         "[]",
655                         m);
656                 layoutProperties.add(marginProperty);
657                 marginProperty.setPriority(PRIO_LAST);
658 
659                 for (XmlProperty p : m) {
660                     p.setParent(marginProperty);
661                 }
662             }
663             Property[] l = layoutProperties.toArray(new Property[layoutProperties.size()]);
664             Arrays.sort(l, Property.PRIORITY);
665             Property property = new ComplexProperty(
666                     "Layout Parameters",
667                     "[]",
668                     l);
669             for (Property p : l) {
670                 if (p instanceof XmlProperty) {
671                     ((XmlProperty) p).setParent(property);
672                 }
673             }
674             property.setCategory(PropertyCategory.PREFERRED);
675             collapsed.add(property);
676             property.setPriority(PRIO_SECOND);
677         }
678 
679         if (deprecatedProperties != null && deprecatedProperties.size() > 0) {
680             Property property = new ComplexProperty(
681                     "Deprecated",
682                     "(Deprecated Properties)",
683                     deprecatedProperties.toArray(new Property[deprecatedProperties.size()]));
684             property.setPriority(PRIO_LAST);
685             collapsed.add(property);
686         }
687 
688         Collections.sort(collapsed, Property.PRIORITY);
689 
690         return collapsed;
691     }
692 
693     @Nullable
getGraphicalEditor()694     GraphicalEditorPart getGraphicalEditor() {
695         return mGraphicalEditorPart;
696     }
697 
698     // HACK: This should be passed into each property instead
getCurrentViewObject()699     public Object getCurrentViewObject() {
700         return mCurrentViewCookie;
701     }
702 
setSortingMode(SortingMode sortingMode)703     public void setSortingMode(SortingMode sortingMode) {
704         mSortMode = sortingMode;
705     }
706 
707     // https://bugs.eclipse.org/bugs/show_bug.cgi?id=388574
addWorkaround(Composite parent)708     public static Composite addWorkaround(Composite parent) {
709         if (ButtonPropertyEditorPresentation.isInWorkaround) {
710             Composite top = new Composite(parent, SWT.NONE);
711             top.setLayout(new GridLayout(1, false));
712             Label label = new Label(top, SWT.WRAP);
713             label.setText(
714                     "This dialog is shown instead of an inline text editor as a\n" +
715                     "workaround for an Eclipse bug specific to OSX Mountain Lion.\n" +
716                     "It should be fixed in Eclipse 4.3.");
717             label.setForeground(top.getDisplay().getSystemColor(SWT.COLOR_RED));
718             GridData data = new GridData();
719             data.grabExcessVerticalSpace = false;
720             data.grabExcessHorizontalSpace = false;
721             data.horizontalAlignment = GridData.FILL;
722             data.verticalAlignment = GridData.BEGINNING;
723             label.setLayoutData(data);
724 
725             Link link = new Link(top, SWT.NO_FOCUS);
726             link.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1));
727             link.setText("<a>https://bugs.eclipse.org/bugs/show_bug.cgi?id=388574</a>");
728             link.addSelectionListener(new SelectionAdapter() {
729                 @Override
730                 public void widgetSelected(SelectionEvent event) {
731                     try {
732                         IWorkbench workbench = PlatformUI.getWorkbench();
733                         IWebBrowser browser = workbench.getBrowserSupport().getExternalBrowser();
734                         browser.openURL(new URL(event.text));
735                     } catch (Exception e) {
736                         String message = String.format(
737                                 "Could not open browser. Vist\n%1$s\ninstead.",
738                                 event.text);
739                         MessageDialog.openError(((Link)event.getSource()).getShell(),
740                                 "Browser Error", message);
741                     }
742                 }
743             });
744 
745             return top;
746         }
747 
748         return null;
749     }
750 }
751