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 */
16package com.android.ide.eclipse.adt.internal.editors.layout.properties;
17
18import static com.android.SdkConstants.ATTR_ID;
19import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
20import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
21
22import com.android.annotations.Nullable;
23import com.android.ide.common.api.IAttributeInfo;
24import com.android.ide.common.api.IAttributeInfo.Format;
25import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
26import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
27import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
28import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
29import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
30import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
31import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
32import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
33import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
34import com.android.tools.lint.detector.api.LintUtils;
35import com.google.common.collect.ArrayListMultimap;
36import com.google.common.collect.Lists;
37import com.google.common.collect.Maps;
38import com.google.common.collect.Multimap;
39
40import org.eclipse.jface.dialogs.MessageDialog;
41import org.eclipse.swt.SWT;
42import org.eclipse.swt.events.SelectionAdapter;
43import org.eclipse.swt.events.SelectionEvent;
44import org.eclipse.swt.layout.GridData;
45import org.eclipse.swt.layout.GridLayout;
46import org.eclipse.swt.widgets.Composite;
47import org.eclipse.swt.widgets.Label;
48import org.eclipse.swt.widgets.Link;
49import org.eclipse.ui.IWorkbench;
50import org.eclipse.ui.PlatformUI;
51import org.eclipse.ui.browser.IWebBrowser;
52import org.eclipse.wb.internal.core.editor.structure.property.PropertyListIntersector;
53import org.eclipse.wb.internal.core.model.property.ComplexProperty;
54import org.eclipse.wb.internal.core.model.property.Property;
55import org.eclipse.wb.internal.core.model.property.category.PropertyCategory;
56import org.eclipse.wb.internal.core.model.property.editor.PropertyEditor;
57import org.eclipse.wb.internal.core.model.property.editor.presentation.ButtonPropertyEditorPresentation;
58
59import java.net.URL;
60import java.util.ArrayList;
61import java.util.Arrays;
62import java.util.Collection;
63import java.util.Collections;
64import java.util.EnumSet;
65import java.util.HashMap;
66import java.util.HashSet;
67import java.util.List;
68import java.util.Map;
69import java.util.Set;
70import 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 */
81public 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
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     */
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
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
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
254    protected Collection<? extends Property> sortAlphabetically(
255            UiViewElementNode node,
256            List<XmlProperty> properties) {
257        Collections.sort(properties, Property.ALPHABETICAL);
258        return properties;
259    }
260
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
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
694    GraphicalEditorPart getGraphicalEditor() {
695        return mGraphicalEditorPart;
696    }
697
698    // HACK: This should be passed into each property instead
699    public Object getCurrentViewObject() {
700        return mCurrentViewCookie;
701    }
702
703    public void setSortingMode(SortingMode sortingMode) {
704        mSortMode = sortingMode;
705    }
706
707    // https://bugs.eclipse.org/bugs/show_bug.cgi?id=388574
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