1 /*
2  * Copyright (C) 2009 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 
17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
18 
19 import static com.android.SdkConstants.ANDROID_URI;
20 import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
21 import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
22 import static com.android.SdkConstants.ATTR_TEXT;
23 import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
24 import static com.android.SdkConstants.XMLNS_ANDROID;
25 import static com.android.SdkConstants.XMLNS_URI;
26 
27 import com.android.ide.common.api.InsertType;
28 import com.android.ide.common.api.Rect;
29 import com.android.ide.common.api.RuleAction.Toggle;
30 import com.android.ide.common.rendering.LayoutLibrary;
31 import com.android.ide.common.rendering.api.Capability;
32 import com.android.ide.common.rendering.api.LayoutLog;
33 import com.android.ide.common.rendering.api.RenderSession;
34 import com.android.ide.common.rendering.api.ViewInfo;
35 import com.android.ide.eclipse.adt.AdtPlugin;
36 import com.android.ide.eclipse.adt.internal.editors.IconFactory;
37 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
38 import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
39 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
40 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
41 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
42 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService;
43 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
44 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
45 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
46 import com.android.ide.eclipse.adt.internal.editors.layout.gre.PaletteMetadataDescriptor;
47 import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
48 import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository.RenderMode;
49 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
50 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
51 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
52 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
53 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
54 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
55 import com.android.sdklib.IAndroidTarget;
56 import com.android.utils.Pair;
57 
58 import org.eclipse.jface.action.Action;
59 import org.eclipse.jface.action.IAction;
60 import org.eclipse.jface.action.IToolBarManager;
61 import org.eclipse.jface.action.MenuManager;
62 import org.eclipse.jface.action.Separator;
63 import org.eclipse.jface.resource.ImageDescriptor;
64 import org.eclipse.swt.SWT;
65 import org.eclipse.swt.custom.CLabel;
66 import org.eclipse.swt.dnd.DND;
67 import org.eclipse.swt.dnd.DragSource;
68 import org.eclipse.swt.dnd.DragSourceEvent;
69 import org.eclipse.swt.dnd.DragSourceListener;
70 import org.eclipse.swt.dnd.Transfer;
71 import org.eclipse.swt.events.DisposeEvent;
72 import org.eclipse.swt.events.DisposeListener;
73 import org.eclipse.swt.events.MenuDetectEvent;
74 import org.eclipse.swt.events.MenuDetectListener;
75 import org.eclipse.swt.events.MouseAdapter;
76 import org.eclipse.swt.events.MouseEvent;
77 import org.eclipse.swt.events.MouseTrackListener;
78 import org.eclipse.swt.events.SelectionAdapter;
79 import org.eclipse.swt.events.SelectionEvent;
80 import org.eclipse.swt.graphics.Color;
81 import org.eclipse.swt.graphics.GC;
82 import org.eclipse.swt.graphics.Image;
83 import org.eclipse.swt.graphics.ImageData;
84 import org.eclipse.swt.graphics.Point;
85 import org.eclipse.swt.graphics.RGB;
86 import org.eclipse.swt.graphics.Rectangle;
87 import org.eclipse.swt.layout.FillLayout;
88 import org.eclipse.swt.layout.GridData;
89 import org.eclipse.swt.layout.GridLayout;
90 import org.eclipse.swt.widgets.Button;
91 import org.eclipse.swt.widgets.Composite;
92 import org.eclipse.swt.widgets.Control;
93 import org.eclipse.swt.widgets.Display;
94 import org.eclipse.swt.widgets.Menu;
95 import org.eclipse.swt.widgets.ToolBar;
96 import org.eclipse.swt.widgets.ToolItem;
97 import org.eclipse.wb.internal.core.editor.structure.IPage;
98 import org.w3c.dom.Attr;
99 import org.w3c.dom.Document;
100 import org.w3c.dom.Element;
101 
102 import java.awt.image.BufferedImage;
103 import java.io.IOException;
104 import java.io.StringWriter;
105 import java.util.ArrayList;
106 import java.util.Collection;
107 import java.util.Collections;
108 import java.util.HashMap;
109 import java.util.List;
110 import java.util.Map;
111 import java.util.Set;
112 
113 /**
114  * A palette control for the {@link GraphicalEditorPart}.
115  * <p/>
116  * The palette contains several groups, each with a UI name (e.g. layouts and views) and each
117  * with a list of element descriptors.
118  * <p/>
119  *
120  * TODO list:
121  *   - The available items should depend on the actual GLE2 Canvas selection. Selected android
122  *     views should force filtering on what they accept can be dropped on them (e.g. TabHost,
123  *     TableLayout). Should enable/disable them, not hide them, to avoid shuffling around.
124  *   - Optional: a text filter
125  *   - Optional: have context-sensitive tools items, e.g. selection arrow tool,
126  *     group selection tool, alignment, etc.
127  */
128 public class PaletteControl extends Composite {
129 
130     /**
131      * Wrapper to create a {@link PaletteControl}
132      */
133     static class PalettePage implements IPage {
134         private final GraphicalEditorPart mEditorPart;
135         private PaletteControl mControl;
136 
PalettePage(GraphicalEditorPart editor)137         PalettePage(GraphicalEditorPart editor) {
138             mEditorPart = editor;
139         }
140 
141         @Override
createControl(Composite parent)142         public void createControl(Composite parent) {
143             mControl = new PaletteControl(parent, mEditorPart);
144         }
145 
146         @Override
getControl()147         public Control getControl() {
148             return mControl;
149         }
150 
151         @Override
dispose()152         public void dispose() {
153             mControl.dispose();
154         }
155 
156         @Override
setToolBar(IToolBarManager toolBarManager)157         public void setToolBar(IToolBarManager toolBarManager) {
158         }
159 
160         /**
161          * Add tool bar items to the given toolbar
162          *
163          * @param toolbar the toolbar to add items into
164          */
createToolbarItems(final ToolBar toolbar)165         void createToolbarItems(final ToolBar toolbar) {
166             final ToolItem popupMenuItem = new ToolItem(toolbar, SWT.PUSH);
167             popupMenuItem.setToolTipText("View Menu");
168             popupMenuItem.setImage(IconFactory.getInstance().getIcon("view_menu"));
169             popupMenuItem.addSelectionListener(new SelectionAdapter() {
170                 @Override
171                 public void widgetSelected(SelectionEvent e) {
172                     Rectangle bounds = popupMenuItem.getBounds();
173                     // Align menu horizontally with the toolbar button and
174                     // vertically with the bottom of the toolbar
175                     Point point = toolbar.toDisplay(bounds.x, bounds.y + bounds.height);
176                     mControl.showMenu(point.x, point.y);
177                 }
178             });
179         }
180 
181         @Override
setFocus()182         public void setFocus() {
183             mControl.setFocus();
184         }
185     }
186 
187     /**
188      * The parent grid layout that contains all the {@link Toggle} and
189      * {@link IconTextItem} widgets.
190      */
191     private GraphicalEditorPart mEditor;
192     private Color mBackground;
193     private Color mForeground;
194 
195     /** The palette modes control various ways to visualize and lay out the views */
196     private static enum PaletteMode {
197         /** Show rendered previews of the views */
198         PREVIEW("Show Previews", true),
199         /** Show rendered previews of the views, scaled down to 75% */
200         SMALL_PREVIEW("Show Small Previews", true),
201         /** Show rendered previews of the views, scaled down to 50% */
202         TINY_PREVIEW("Show Tiny Previews", true),
203         /** Show an icon + text label */
204         ICON_TEXT("Show Icon and Text", false),
205         /** Show only icons, packed multiple per row */
206         ICON_ONLY("Show Only Icons", true);
207 
PaletteMode(String actionLabel, boolean wrap)208         PaletteMode(String actionLabel, boolean wrap) {
209             mActionLabel = actionLabel;
210             mWrap = wrap;
211         }
212 
getActionLabel()213         public String getActionLabel() {
214             return mActionLabel;
215         }
216 
getWrap()217         public boolean getWrap() {
218             return mWrap;
219         }
220 
isPreview()221         public boolean isPreview() {
222             return this == PREVIEW || this == SMALL_PREVIEW || this == TINY_PREVIEW;
223         }
224 
isScaledPreview()225         public boolean isScaledPreview() {
226             return this == SMALL_PREVIEW || this == TINY_PREVIEW;
227         }
228 
229         private final String mActionLabel;
230         private final boolean mWrap;
231     };
232 
233     /** Token used in preference string to record alphabetical sorting */
234     private static final String VALUE_ALPHABETICAL = "alpha";   //$NON-NLS-1$
235     /** Token used in preference string to record categories being turned off */
236     private static final String VALUE_NO_CATEGORIES = "nocat"; //$NON-NLS-1$
237     /** Token used in preference string to record auto close being turned off */
238     private static final String VALUE_NO_AUTOCLOSE = "noauto";      //$NON-NLS-1$
239 
240     private final PreviewIconFactory mPreviewIconFactory = new PreviewIconFactory(this);
241     private PaletteMode mPaletteMode = null;
242     /** Use alphabetical sorting instead of natural order? */
243     private boolean mAlphabetical;
244     /** Use categories instead of a single large list of views? */
245     private boolean mCategories = true;
246     /** Auto-close the previous category when new categories are opened */
247     private boolean mAutoClose = true;
248     private AccordionControl mAccordion;
249     private String mCurrentTheme;
250     private String mCurrentDevice;
251     private IAndroidTarget mCurrentTarget;
252     private AndroidTargetData mCurrentTargetData;
253 
254     /**
255      * Create the composite.
256      * @param parent The parent composite.
257      * @param editor An editor associated with this palette.
258      */
PaletteControl(Composite parent, GraphicalEditorPart editor)259     public PaletteControl(Composite parent, GraphicalEditorPart editor) {
260         super(parent, SWT.NONE);
261 
262         mEditor = editor;
263     }
264 
265     /** Reads UI mode from persistent store to preserve palette mode across IDE sessions */
loadPaletteMode()266     private void loadPaletteMode() {
267         String paletteModes = AdtPrefs.getPrefs().getPaletteModes();
268         if (paletteModes.length() > 0) {
269             String[] tokens = paletteModes.split(","); //$NON-NLS-1$
270             try {
271                 mPaletteMode = PaletteMode.valueOf(tokens[0]);
272             } catch (Throwable t) {
273                 mPaletteMode = PaletteMode.values()[0];
274             }
275             mAlphabetical = paletteModes.contains(VALUE_ALPHABETICAL);
276             mCategories = !paletteModes.contains(VALUE_NO_CATEGORIES);
277             mAutoClose = !paletteModes.contains(VALUE_NO_AUTOCLOSE);
278         } else {
279             mPaletteMode = PaletteMode.SMALL_PREVIEW;
280         }
281     }
282 
283     /**
284      * Returns the most recently stored version of auto-close-mode; this is the last
285      * user-initiated setting of the auto-close mode (we programmatically switch modes when
286      * you enter icons-only mode, and set it back to this when going to any other mode)
287      */
getSavedAutoCloseMode()288     private boolean getSavedAutoCloseMode() {
289         return !AdtPrefs.getPrefs().getPaletteModes().contains(VALUE_NO_AUTOCLOSE);
290     }
291 
292     /** Saves UI mode to persistent store to preserve palette mode across IDE sessions */
savePaletteMode()293     private void savePaletteMode() {
294         StringBuilder sb = new StringBuilder();
295         sb.append(mPaletteMode);
296         if (mAlphabetical) {
297             sb.append(',').append(VALUE_ALPHABETICAL);
298         }
299         if (!mCategories) {
300             sb.append(',').append(VALUE_NO_CATEGORIES);
301         }
302         if (!mAutoClose) {
303             sb.append(',').append(VALUE_NO_AUTOCLOSE);
304         }
305         AdtPrefs.getPrefs().setPaletteModes(sb.toString());
306     }
307 
refreshPalette()308     private void refreshPalette() {
309         IAndroidTarget oldTarget = mCurrentTarget;
310         mCurrentTarget = null;
311         mCurrentTargetData = null;
312         mCurrentTheme = null;
313         mCurrentDevice = null;
314         reloadPalette(oldTarget);
315     }
316 
317     @Override
checkSubclass()318     protected void checkSubclass() {
319         // Disable the check that prevents subclassing of SWT components
320     }
321 
322     @Override
dispose()323     public void dispose() {
324         if (mBackground != null) {
325             mBackground.dispose();
326             mBackground = null;
327         }
328         if (mForeground != null) {
329             mForeground.dispose();
330             mForeground = null;
331         }
332 
333         super.dispose();
334     }
335 
336     /**
337      * Returns the currently displayed target
338      *
339      * @return the current target, or null
340      */
getCurrentTarget()341     public IAndroidTarget getCurrentTarget() {
342         return mCurrentTarget;
343     }
344 
345     /**
346      * Returns the currently displayed theme (in palette modes that support previewing)
347      *
348      * @return the current theme, or null
349      */
getCurrentTheme()350     public String getCurrentTheme() {
351         return mCurrentTheme;
352     }
353 
354     /**
355      * Returns the currently displayed device (in palette modes that support previewing)
356      *
357      * @return the current device, or null
358      */
getCurrentDevice()359     public String getCurrentDevice() {
360         return mCurrentDevice;
361     }
362 
363     /** Returns true if previews in the palette should be made available */
previewsAvailable()364     private boolean previewsAvailable() {
365         // Not layoutlib 5 -- we require custom background support to do
366         // a decent job with previews
367         LayoutLibrary layoutLibrary = mEditor.getLayoutLibrary();
368         return layoutLibrary != null && layoutLibrary.supports(Capability.CUSTOM_BACKGROUND_COLOR);
369     }
370 
371     /**
372      * Loads or reloads the palette elements by using the layout and view descriptors from the
373      * given target data.
374      *
375      * @param target The target that has just been loaded
376      */
reloadPalette(IAndroidTarget target)377     public void reloadPalette(IAndroidTarget target) {
378         ConfigurationChooser configChooser = mEditor.getConfigurationChooser();
379         String theme = configChooser.getThemeName();
380         String device = configChooser.getDeviceName();
381         if (device == null) {
382             return;
383         }
384         AndroidTargetData targetData =
385             target != null ? Sdk.getCurrent().getTargetData(target) : null;
386         if (target == mCurrentTarget && targetData == mCurrentTargetData
387                 && mCurrentTheme != null && mCurrentTheme.equals(theme)
388                 && mCurrentDevice != null && mCurrentDevice.equals(device)) {
389             return;
390         }
391         mCurrentTheme = theme;
392         mCurrentTarget = target;
393         mCurrentTargetData = targetData;
394         mCurrentDevice = device;
395         mPreviewIconFactory.reset();
396 
397         if (targetData == null) {
398             return;
399         }
400 
401         Set<String> expandedCategories = null;
402         if (mAccordion != null) {
403             expandedCategories = mAccordion.getExpandedCategories();
404             // We auto-expand all categories when showing icons-only. When returning to some
405             // other mode we don't want to retain all categories open.
406             if (expandedCategories.size() > 3) {
407                 expandedCategories = null;
408             }
409         }
410 
411         // Erase old content and recreate new
412         for (Control c : getChildren()) {
413             c.dispose();
414         }
415 
416         if (mPaletteMode == null) {
417             loadPaletteMode();
418             assert mPaletteMode != null;
419         }
420 
421         // Ensure that the palette mode is supported on this version of the layout library
422         if (!previewsAvailable()) {
423             if (mPaletteMode.isPreview()) {
424                 mPaletteMode = PaletteMode.ICON_TEXT;
425             }
426         }
427 
428         if (mPaletteMode.isPreview()) {
429             if (mForeground != null) {
430                 mForeground.dispose();
431                 mForeground = null;
432             }
433             if (mBackground != null) {
434                 mBackground.dispose();
435                 mBackground = null;
436             }
437             RGB background = mPreviewIconFactory.getBackgroundColor();
438             if (background != null) {
439                 mBackground = new Color(getDisplay(), background);
440             }
441             RGB foreground = mPreviewIconFactory.getForegroundColor();
442             if (foreground != null) {
443                 mForeground = new Color(getDisplay(), foreground);
444             }
445         }
446 
447         List<String> headers = Collections.emptyList();
448         final Map<String, List<ViewElementDescriptor>> categoryToItems;
449         categoryToItems = new HashMap<String, List<ViewElementDescriptor>>();
450         headers = new ArrayList<String>();
451         List<Pair<String,List<ViewElementDescriptor>>> paletteEntries =
452             ViewMetadataRepository.get().getPaletteEntries(targetData,
453                     mAlphabetical, mCategories);
454         for (Pair<String,List<ViewElementDescriptor>> pair : paletteEntries) {
455             String category = pair.getFirst();
456             List<ViewElementDescriptor> categoryItems = pair.getSecond();
457             headers.add(category);
458             categoryToItems.put(category, categoryItems);
459         }
460 
461         headers.add("Custom & Library Views");
462 
463         // Set the categories to expand the first item if
464         //   (1) we don't have a previously selected category, or
465         //   (2) there's just one category anyway, or
466         //   (3) the set of categories have changed so our previously selected category
467         //       doesn't exist anymore (can happen when you toggle "Show Categories")
468         if ((expandedCategories == null && headers.size() > 0) || headers.size() == 1 ||
469                 (expandedCategories != null && expandedCategories.size() >= 1
470                         && !headers.contains(
471                                 expandedCategories.iterator().next().replace("&&", "&")))) { //$NON-NLS-1$ //$NON-NLS-2$
472             // Expand the first category if we don't have a previous selection (e.g. refresh)
473             expandedCategories = Collections.singleton(headers.get(0));
474         }
475 
476         boolean wrap = mPaletteMode.getWrap();
477 
478         // Pack icon-only view vertically; others stretch to fill palette region
479         boolean fillVertical = mPaletteMode != PaletteMode.ICON_ONLY;
480 
481         mAccordion = new AccordionControl(this, SWT.NONE, headers, fillVertical, wrap,
482                 expandedCategories) {
483             @Override
484             protected Composite createChildContainer(Composite parent, Object header, int style) {
485                 assert categoryToItems != null;
486                 List<ViewElementDescriptor> list = categoryToItems.get(header);
487                 final Composite composite;
488                 if (list == null) {
489                     assert header.equals("Custom & Library Views");
490 
491                     Composite wrapper = new Composite(parent, SWT.NONE);
492                     GridLayout gridLayout = new GridLayout(1, false);
493                     gridLayout.marginWidth = gridLayout.marginHeight = 0;
494                     gridLayout.horizontalSpacing = gridLayout.verticalSpacing = 0;
495                     gridLayout.marginBottom = 3;
496                     wrapper.setLayout(gridLayout);
497                     if (mPaletteMode.isPreview() && mBackground != null) {
498                         wrapper.setBackground(mBackground);
499                     }
500                     composite = super.createChildContainer(wrapper, header, style);
501                     if (mPaletteMode.isPreview() && mBackground != null) {
502                         composite.setBackground(mBackground);
503                     }
504                     composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
505 
506                     Button refreshButton = new Button(wrapper, SWT.PUSH | SWT.FLAT);
507                     refreshButton.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER,
508                             false, false, 1, 1));
509                     refreshButton.setText("Refresh");
510                     refreshButton.setImage(IconFactory.getInstance().getIcon("refresh")); //$NON-NLS-1$
511                     refreshButton.addSelectionListener(new SelectionAdapter() {
512                         @Override
513                         public void widgetSelected(SelectionEvent e) {
514                             CustomViewFinder finder = CustomViewFinder.get(mEditor.getProject());
515                             finder.refresh(new ViewFinderListener(composite));
516                         }
517                     });
518 
519                     wrapper.layout(true);
520                 } else {
521                     composite = super.createChildContainer(parent, header, style);
522                     if (mPaletteMode.isPreview() && mBackground != null) {
523                         composite.setBackground(mBackground);
524                     }
525                 }
526                 addMenu(composite);
527                 return composite;
528             }
529             @Override
530             protected void createChildren(Composite parent, Object header) {
531                 assert categoryToItems != null;
532                 List<ViewElementDescriptor> list = categoryToItems.get(header);
533                 if (list == null) {
534                     assert header.equals("Custom & Library Views");
535                     addCustomItems(parent);
536                     return;
537                 } else {
538                     for (ViewElementDescriptor desc : list) {
539                         createItem(parent, desc);
540                     }
541                 }
542             }
543         };
544         addMenu(mAccordion);
545         for (CLabel headerLabel : mAccordion.getHeaderLabels()) {
546             addMenu(headerLabel);
547         }
548         setLayout(new FillLayout());
549 
550         // Expand All for icon-only mode, but don't store it as the persistent auto-close mode;
551         // when we enter other modes it will read back whatever persistent mode.
552         if (mPaletteMode == PaletteMode.ICON_ONLY) {
553             mAccordion.expandAll(true);
554             mAccordion.setAutoClose(false);
555         } else {
556             mAccordion.setAutoClose(getSavedAutoCloseMode());
557         }
558 
559         layout(true);
560     }
561 
addCustomItems(final Composite parent)562     protected void addCustomItems(final Composite parent) {
563         final CustomViewFinder finder = CustomViewFinder.get(mEditor.getProject());
564         Collection<String> allViews = finder.getAllViews();
565         if (allViews == null) { // Not yet initialized: trigger an async refresh
566             finder.refresh(new ViewFinderListener(parent));
567             return;
568         }
569 
570         // Remove previous content
571         for (Control c : parent.getChildren()) {
572             c.dispose();
573         }
574 
575         // Add new views
576         for (final String fqcn : allViews) {
577             CustomViewDescriptorService service = CustomViewDescriptorService.getInstance();
578             ViewElementDescriptor desc = service.getDescriptor(mEditor.getProject(), fqcn);
579             if (desc == null) {
580                 // The descriptor lookup performs validation steps of the class, and may
581                 // in some cases determine that this is not a view and will return null;
582                 // guard against that.
583                 continue;
584             }
585 
586             Control item = createItem(parent, desc);
587 
588             // Add control-click listener on custom view items to you can warp to
589             // (and double click listener too -- the more discoverable, the better.)
590             if (item instanceof IconTextItem) {
591                 IconTextItem it = (IconTextItem) item;
592                 it.addMouseListener(new MouseAdapter() {
593                     @Override
594                     public void mouseDoubleClick(MouseEvent e) {
595                         AdtPlugin.openJavaClass(mEditor.getProject(), fqcn);
596                     }
597 
598                     @Override
599                     public void mouseDown(MouseEvent e) {
600                         if ((e.stateMask & SWT.MOD1) != 0) {
601                             AdtPlugin.openJavaClass(mEditor.getProject(), fqcn);
602                         }
603                     }
604                 });
605             }
606         }
607     }
608 
getEditor()609     /* package */ GraphicalEditorPart getEditor() {
610         return mEditor;
611     }
612 
createItem(Composite parent, ViewElementDescriptor desc)613     private Control createItem(Composite parent, ViewElementDescriptor desc) {
614         Control item = null;
615         switch (mPaletteMode) {
616             case SMALL_PREVIEW:
617             case TINY_PREVIEW:
618             case PREVIEW: {
619                 ImageDescriptor descriptor = mPreviewIconFactory.getImageDescriptor(desc);
620                 if (descriptor != null) {
621                     Image image = descriptor.createImage();
622                     ImageControl imageControl = new ImageControl(parent, SWT.None, image);
623                     if (mPaletteMode.isScaledPreview()) {
624                         // Try to preserve the overall size since rendering sizes typically
625                         // vary with the dpi - so while the scaling factor for a 160 dpi
626                         // rendering the scaling factor should be 0.5, for a 320 dpi one the
627                         // scaling factor should be half that, 0.25.
628                         float scale = 1.0f;
629                         if (mPaletteMode == PaletteMode.SMALL_PREVIEW) {
630                             scale = 0.75f;
631                         } else if (mPaletteMode == PaletteMode.TINY_PREVIEW) {
632                             scale = 0.5f;
633                         }
634                         ConfigurationChooser chooser = mEditor.getConfigurationChooser();
635                         int dpi = chooser.getConfiguration().getDensity().getDpiValue();
636                         while (dpi > 160) {
637                             scale = scale / 2;
638                             dpi = dpi / 2;
639                         }
640                         imageControl.setScale(scale);
641                     }
642                     imageControl.setHoverColor(getDisplay().getSystemColor(SWT.COLOR_WHITE));
643                     if (mBackground != null) {
644                         imageControl.setBackground(mBackground);
645                     }
646                     String toolTip = desc.getUiName();
647                     // It appears pretty much none of the descriptors have tooltips
648                     //String descToolTip = desc.getTooltip();
649                     //if (descToolTip != null && descToolTip.length() > 0) {
650                     //    toolTip = toolTip + "\n" + descToolTip;
651                     //}
652                     imageControl.setToolTipText(toolTip);
653 
654                     item = imageControl;
655                 } else {
656                     // Just use an Icon+Text item for these for now
657                     item = new IconTextItem(parent, desc);
658                     if (mForeground != null) {
659                         item.setForeground(mForeground);
660                         item.setBackground(mBackground);
661                     }
662                 }
663                 break;
664             }
665             case ICON_TEXT: {
666                 item = new IconTextItem(parent, desc);
667                 break;
668             }
669             case ICON_ONLY: {
670                 item = new ImageControl(parent, SWT.None, desc.getGenericIcon());
671                 item.setToolTipText(desc.getUiName());
672                 break;
673             }
674             default:
675                 throw new IllegalArgumentException("Not yet implemented");
676         }
677 
678         final DragSource source = new DragSource(item, DND.DROP_COPY);
679         source.setTransfer(new Transfer[] { SimpleXmlTransfer.getInstance() });
680         source.addDragListener(new DescDragSourceListener(desc));
681         item.addDisposeListener(new DisposeListener() {
682             @Override
683             public void widgetDisposed(DisposeEvent e) {
684                 source.dispose();
685             }
686         });
687         addMenu(item);
688 
689         return item;
690     }
691 
692     /**
693      * An Item widget represents one {@link ElementDescriptor} that can be dropped on the
694      * GLE2 canvas using drag'n'drop.
695      */
696     private static class IconTextItem extends CLabel implements MouseTrackListener {
697 
698         private boolean mMouseIn;
699 
IconTextItem(Composite parent, ViewElementDescriptor desc)700         public IconTextItem(Composite parent, ViewElementDescriptor desc) {
701             super(parent, SWT.NONE);
702             mMouseIn = false;
703 
704             setText(desc.getUiName());
705             setImage(desc.getGenericIcon());
706             setToolTipText(desc.getTooltip());
707             addMouseTrackListener(this);
708         }
709 
710         @Override
getStyle()711         public int getStyle() {
712             int style = super.getStyle();
713             if (mMouseIn) {
714                 style |= SWT.SHADOW_IN;
715             }
716             return style;
717         }
718 
719         @Override
mouseEnter(MouseEvent e)720         public void mouseEnter(MouseEvent e) {
721             if (!mMouseIn) {
722                 mMouseIn = true;
723                 redraw();
724             }
725         }
726 
727         @Override
mouseExit(MouseEvent e)728         public void mouseExit(MouseEvent e) {
729             if (mMouseIn) {
730                 mMouseIn = false;
731                 redraw();
732             }
733         }
734 
735         @Override
mouseHover(MouseEvent e)736         public void mouseHover(MouseEvent e) {
737             // pass
738         }
739     }
740 
741     /**
742      * A {@link DragSourceListener} that deals with drag'n'drop of
743      * {@link ElementDescriptor}s.
744      */
745     private class DescDragSourceListener implements DragSourceListener {
746         private final ViewElementDescriptor mDesc;
747         private SimpleElement[] mElements;
748 
DescDragSourceListener(ViewElementDescriptor desc)749         public DescDragSourceListener(ViewElementDescriptor desc) {
750             mDesc = desc;
751         }
752 
753         @Override
dragStart(DragSourceEvent e)754         public void dragStart(DragSourceEvent e) {
755             // See if we can find out the bounds of this element from a preview image.
756             // Preview images are created before the drag source listener is notified
757             // of the started drag.
758             Rect bounds = null;
759             Rect dragBounds = null;
760 
761             createDragImage(e);
762             if (mImage != null && !mIsPlaceholder) {
763                 int width = mImageLayoutBounds.width;
764                 int height = mImageLayoutBounds.height;
765                 assert mImageLayoutBounds.x == 0;
766                 assert mImageLayoutBounds.y == 0;
767                 bounds = new Rect(0, 0, width, height);
768                 double scale = mEditor.getCanvasControl().getScale();
769                 int scaledWidth = (int) (scale * width);
770                 int scaledHeight = (int) (scale * height);
771                 int x = -scaledWidth / 2;
772                 int y = -scaledHeight / 2;
773                 dragBounds = new Rect(x, y, scaledWidth, scaledHeight);
774             }
775 
776             SimpleElement se = new SimpleElement(
777                     SimpleXmlTransfer.getFqcn(mDesc),
778                     null   /* parentFqcn */,
779                     bounds /* bounds */,
780                     null   /* parentBounds */);
781             if (mDesc instanceof PaletteMetadataDescriptor) {
782                 PaletteMetadataDescriptor pm = (PaletteMetadataDescriptor) mDesc;
783                 pm.initializeNew(se);
784             }
785             mElements = new SimpleElement[] { se };
786 
787             // Register this as the current dragged data
788             GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance();
789             dragInfo.startDrag(
790                     mElements,
791                     null /* selection */,
792                     null /* canvas */,
793                     null /* removeSource */);
794             dragInfo.setDragBounds(dragBounds);
795             dragInfo.setDragBaseline(mBaseline);
796 
797 
798             e.doit = true;
799         }
800 
801         @Override
dragSetData(DragSourceEvent e)802         public void dragSetData(DragSourceEvent e) {
803             // Provide the data for the drop when requested by the other side.
804             if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) {
805                 e.data = mElements;
806             }
807         }
808 
809         @Override
dragFinished(DragSourceEvent e)810         public void dragFinished(DragSourceEvent e) {
811             // Unregister the dragged data.
812             GlobalCanvasDragInfo.getInstance().stopDrag();
813             mElements = null;
814             if (mImage != null) {
815                 mImage.dispose();
816                 mImage = null;
817             }
818         }
819 
820         // TODO: Figure out the right dimensions to use for rendering.
821         // We WILL crop this after rendering, but for performance reasons it would be good
822         // not to make it much larger than necessary since to crop this we rely on
823         // actually scanning pixels.
824 
825         /**
826          * Width of the rendered preview image (before it is cropped), although the actual
827          * width may be smaller (since we also take the device screen's size into account)
828          */
829         private static final int MAX_RENDER_HEIGHT = 400;
830 
831         /**
832          * Height of the rendered preview image (before it is cropped), although the
833          * actual width may be smaller (since we also take the device screen's size into
834          * account)
835          */
836         private static final int MAX_RENDER_WIDTH = 500;
837 
838         /** Amount of alpha to multiply into the image (divided by 256) */
839         private static final int IMG_ALPHA = 128;
840 
841         /** The image shown during the drag */
842         private Image mImage;
843         /** The non-effect bounds of the drag image */
844         private Rectangle mImageLayoutBounds;
845         private int mBaseline = -1;
846 
847         /**
848          * If true, the image is a preview of the view, and if not it is a "fallback"
849          * image of some sort, such as a rendering of the palette item itself
850          */
851         private boolean mIsPlaceholder;
852 
createDragImage(DragSourceEvent event)853         private void createDragImage(DragSourceEvent event) {
854             mBaseline = -1;
855             Pair<Image, Rectangle> preview = renderPreview();
856             if (preview != null) {
857                 mImage = preview.getFirst();
858                 mImageLayoutBounds = preview.getSecond();
859             } else {
860                 mImage = null;
861                 mImageLayoutBounds = null;
862             }
863 
864             mIsPlaceholder = mImage == null;
865             if (mIsPlaceholder) {
866                 // Couldn't render preview (or the preview is a blank image, such as for
867                 // example the preview of an empty layout), so instead create a placeholder
868                 // image
869                 // Render the palette item itself as an image
870                 Control control = ((DragSource) event.widget).getControl();
871                 GC gc = new GC(control);
872                 Point size = control.getSize();
873                 Display display = getDisplay();
874                 final Image image = new Image(display, size.x, size.y);
875                 gc.copyArea(image, 0, 0);
876                 gc.dispose();
877 
878                 BufferedImage awtImage = SwtUtils.convertToAwt(image);
879                 if (awtImage != null) {
880                     awtImage = ImageUtils.createDropShadow(awtImage, 3 /* shadowSize */,
881                             0.7f /* shadowAlpha */, 0x000000 /* shadowRgb */);
882                     mImage = SwtUtils.convertToSwt(display, awtImage, true, IMG_ALPHA);
883                 } else {
884                     ImageData data = image.getImageData();
885                     data.alpha = IMG_ALPHA;
886 
887                     // Changing the ImageData -after- constructing an image on it
888                     // has no effect, so we have to construct a new image. Luckily these
889                     // are tiny images.
890                     mImage = new Image(display, data);
891                 }
892                 image.dispose();
893             }
894 
895             event.image = mImage;
896 
897             if (!mIsPlaceholder) {
898                 // Shift the drag feedback image up such that it's centered under the
899                 // mouse pointer
900                 double scale = mEditor.getCanvasControl().getScale();
901                 event.offsetX = (int) (scale * mImageLayoutBounds.width / 2);
902                 event.offsetY = (int) (scale * mImageLayoutBounds.height / 2);
903             }
904         }
905 
906         /**
907          * Performs the actual rendering of the descriptor into an image and returns the
908          * image as well as the layout bounds of the image (not including drop shadow etc)
909          */
renderPreview()910         private Pair<Image, Rectangle> renderPreview() {
911             ViewMetadataRepository repository = ViewMetadataRepository.get();
912             RenderMode renderMode = repository.getRenderMode(mDesc.getFullClassName());
913             if (renderMode == RenderMode.SKIP) {
914                 return null;
915             }
916 
917             // Create blank XML document
918             Document document = DomUtilities.createEmptyDocument();
919 
920             // Insert our target view's XML into it as a node
921             GraphicalEditorPart editor = getEditor();
922             LayoutEditorDelegate layoutEditorDelegate = editor.getEditorDelegate();
923 
924             String viewName = mDesc.getXmlLocalName();
925             Element element = document.createElement(viewName);
926 
927             // Set up a proper name space
928             Attr attr = document.createAttributeNS(XMLNS_URI, XMLNS_ANDROID);
929             attr.setValue(ANDROID_URI);
930             element.getAttributes().setNamedItemNS(attr);
931 
932             element.setAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT);
933             element.setAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
934 
935             // This doesn't apply to all, but doesn't seem to cause harm and makes for a
936             // better experience with text-oriented views like buttons and texts
937             element.setAttributeNS(ANDROID_URI, ATTR_TEXT,
938                     DescriptorsUtils.getBasename(mDesc.getUiName()));
939 
940             // Is this a palette variation?
941             if (mDesc instanceof PaletteMetadataDescriptor) {
942                 PaletteMetadataDescriptor pm = (PaletteMetadataDescriptor) mDesc;
943                 pm.initializeNew(element);
944             }
945 
946             document.appendChild(element);
947 
948             // Construct UI model from XML
949             AndroidTargetData data = layoutEditorDelegate.getEditor().getTargetData();
950             DocumentDescriptor documentDescriptor;
951             if (data == null) {
952                 documentDescriptor = new DocumentDescriptor("temp", null/*children*/);//$NON-NLS-1$
953             } else {
954                 documentDescriptor = data.getLayoutDescriptors().getDescriptor();
955             }
956             UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode();
957             model.setEditor(layoutEditorDelegate.getEditor());
958             model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider());
959             model.loadFromXmlNode(document);
960 
961             // Call the create-hooks such that we for example insert mandatory
962             // children into views like the DialerFilter, apply image source attributes
963             // to ImageButtons, etc.
964             LayoutCanvas canvas = editor.getCanvasControl();
965             NodeFactory nodeFactory = canvas.getNodeFactory();
966             UiElementNode parent = model.getUiRoot();
967             UiElementNode child = parent.getUiChildren().get(0);
968             if (child instanceof UiViewElementNode) {
969                 UiViewElementNode childUiNode = (UiViewElementNode) child;
970                 NodeProxy childNode = nodeFactory.create(childUiNode);
971 
972                 // Applying create hooks as part of palette render should
973                 // not trigger model updates
974                 layoutEditorDelegate.getEditor().setIgnoreXmlUpdate(true);
975                 try {
976                     canvas.getRulesEngine().callCreateHooks(layoutEditorDelegate.getEditor(),
977                             null, childNode, InsertType.CREATE_PREVIEW);
978                     childNode.applyPendingChanges();
979                 } catch (Throwable t) {
980                     AdtPlugin.log(t, "Failed calling creation hooks for widget %1$s", viewName);
981                 } finally {
982                     layoutEditorDelegate.getEditor().setIgnoreXmlUpdate(false);
983                 }
984             }
985 
986             Integer overrideBgColor = null;
987             boolean hasTransparency = false;
988             LayoutLibrary layoutLibrary = editor.getLayoutLibrary();
989             if (layoutLibrary != null &&
990                     layoutLibrary.supports(Capability.CUSTOM_BACKGROUND_COLOR)) {
991                 // It doesn't matter what the background color is as long as the alpha
992                 // is 0 (fully transparent). We're using red to make it more obvious if
993                 // for some reason the background is painted when it shouldn't be.
994                 overrideBgColor = new Integer(0x00FF0000);
995             }
996 
997             RenderSession session = null;
998             try {
999                 // Use at most the size of the screen for the preview render.
1000                 // This is important since when we fill the size of certain views (like
1001                 // a SeekBar), we want it to at most be the width of the screen, and for small
1002                 // screens the RENDER_WIDTH was wider.
1003                 LayoutLog silentLogger = new LayoutLog();
1004 
1005                 session = RenderService.create(editor)
1006                     .setModel(model)
1007                     .setMaxRenderSize(MAX_RENDER_WIDTH, MAX_RENDER_HEIGHT)
1008                     .setLog(silentLogger)
1009                     .setOverrideBgColor(overrideBgColor)
1010                     .setDecorations(false)
1011                     .createRenderSession();
1012             } catch (Throwable t) {
1013                 // Previews can fail for a variety of reasons -- let's not bug
1014                 // the user with it
1015                 return null;
1016             }
1017 
1018             if (session != null) {
1019                 if (session.getResult().isSuccess()) {
1020                     BufferedImage image = session.getImage();
1021                     if (image != null) {
1022                         BufferedImage cropped;
1023                         Rect initialCrop = null;
1024                         ViewInfo viewInfo = null;
1025 
1026                         List<ViewInfo> viewInfoList = session.getRootViews();
1027 
1028                         if (viewInfoList != null && viewInfoList.size() > 0) {
1029                             viewInfo = viewInfoList.get(0);
1030                             mBaseline = viewInfo.getBaseLine();
1031                         }
1032 
1033                         if (viewInfo != null) {
1034                             int x1 = viewInfo.getLeft();
1035                             int x2 = viewInfo.getRight();
1036                             int y2 = viewInfo.getBottom();
1037                             int y1 = viewInfo.getTop();
1038                             initialCrop = new Rect(x1, y1, x2 - x1, y2 - y1);
1039                         }
1040 
1041                         if (hasTransparency) {
1042                             cropped = ImageUtils.cropBlank(image, initialCrop);
1043                         } else {
1044                             // Find out what the "background" color is such that we can properly
1045                             // crop it out of the image. To do this we pick out a pixel in the
1046                             // bottom right unpainted area. Rather than pick the one in the far
1047                             // bottom corner, we pick one as close to the bounds of the view as
1048                             // possible (but still outside of the bounds), such that we can
1049                             // deal with themes like the dialog theme.
1050                             int edgeX = image.getWidth() -1;
1051                             int edgeY = image.getHeight() -1;
1052                             if (viewInfo != null) {
1053                                 if (viewInfo.getRight() < image.getWidth()-1) {
1054                                     edgeX = viewInfo.getRight()+1;
1055                                 }
1056                                 if (viewInfo.getBottom() < image.getHeight()-1) {
1057                                     edgeY = viewInfo.getBottom()+1;
1058                                 }
1059                             }
1060                             int edgeColor = image.getRGB(edgeX, edgeY);
1061                             cropped = ImageUtils.cropColor(image, edgeColor, initialCrop);
1062                         }
1063 
1064                         if (cropped != null) {
1065                             int width = initialCrop != null ? initialCrop.w : cropped.getWidth();
1066                             int height = initialCrop != null ? initialCrop.h : cropped.getHeight();
1067                             boolean needsContrast = hasTransparency
1068                                     && !ImageUtils.containsDarkPixels(cropped);
1069                             cropped = ImageUtils.createDropShadow(cropped,
1070                                     hasTransparency ? 3 : 5 /* shadowSize */,
1071                                     !hasTransparency ? 0.6f : needsContrast ? 0.8f : 0.7f/*alpha*/,
1072                                     0x000000 /* shadowRgb */);
1073 
1074                             double scale = canvas.getScale();
1075                             if (scale != 1L) {
1076                                 cropped = ImageUtils.scale(cropped, scale, scale);
1077                             }
1078 
1079                             Display display = getDisplay();
1080                             int alpha = (!hasTransparency || !needsContrast) ? IMG_ALPHA : -1;
1081                             Image swtImage = SwtUtils.convertToSwt(display, cropped, true, alpha);
1082                             Rectangle imageBounds = new Rectangle(0, 0, width, height);
1083                             return Pair.of(swtImage, imageBounds);
1084                         }
1085                     }
1086                 }
1087 
1088                 session.dispose();
1089             }
1090 
1091             return null;
1092         }
1093 
1094         /**
1095          * Utility method to print out the contents of the given XML document. This is
1096          * really useful when working on the preview code above. I'm including all the
1097          * code inside a constant false, which means the compiler will omit all the code,
1098          * but I'd like to leave it in the code base and by doing it this way rather than
1099          * as commented out code the code won't be accidentally broken.
1100          */
1101         @SuppressWarnings("all")
dumpDocument(Document document)1102         private void dumpDocument(Document document) {
1103             // Diagnostics: print out the XML that we're about to render
1104             if (false) { // Will be omitted by the compiler
1105                 org.apache.xml.serialize.OutputFormat outputFormat =
1106                     new org.apache.xml.serialize.OutputFormat(
1107                             "XML", "ISO-8859-1", true); //$NON-NLS-1$ //$NON-NLS-2$
1108                 outputFormat.setIndent(2);
1109                 outputFormat.setLineWidth(100);
1110                 outputFormat.setIndenting(true);
1111                 outputFormat.setOmitXMLDeclaration(true);
1112                 outputFormat.setOmitDocumentType(true);
1113                 StringWriter stringWriter = new StringWriter();
1114                 // Using FQN here to avoid having an import above, which will result
1115                 // in a deprecation warning, and there isn't a way to annotate a single
1116                 // import element with a SuppressWarnings.
1117                 org.apache.xml.serialize.XMLSerializer serializer =
1118                     new org.apache.xml.serialize.XMLSerializer(stringWriter, outputFormat);
1119                 serializer.setNamespaces(true);
1120                 try {
1121                     serializer.serialize(document.getDocumentElement());
1122                     System.out.println(stringWriter.toString());
1123                 } catch (IOException e) {
1124                     e.printStackTrace();
1125                 }
1126             }
1127         }
1128     }
1129 
1130     /** Action for switching view modes via radio buttons */
1131     private class PaletteModeAction extends Action {
1132         private final PaletteMode mMode;
1133 
PaletteModeAction(PaletteMode mode)1134         PaletteModeAction(PaletteMode mode) {
1135             super(mode.getActionLabel(), IAction.AS_RADIO_BUTTON);
1136             mMode = mode;
1137             boolean selected = mMode == mPaletteMode;
1138             setChecked(selected);
1139             setEnabled(!selected);
1140         }
1141 
1142         @Override
run()1143         public void run() {
1144             if (isEnabled()) {
1145                 mPaletteMode = mMode;
1146                 refreshPalette();
1147                 savePaletteMode();
1148             }
1149         }
1150     }
1151 
1152     /** Action for toggling various checkbox view modes - categories, sorting, etc */
1153     private class ToggleViewOptionAction extends Action {
1154         private final int mAction;
1155         final static int TOGGLE_CATEGORY = 1;
1156         final static int TOGGLE_ALPHABETICAL = 2;
1157         final static int TOGGLE_AUTO_CLOSE = 3;
1158         final static int REFRESH = 4;
1159         final static int RESET = 5;
1160 
ToggleViewOptionAction(String title, int action, boolean checked)1161         ToggleViewOptionAction(String title, int action, boolean checked) {
1162             super(title, (action == REFRESH || action == RESET) ? IAction.AS_PUSH_BUTTON
1163                     : IAction.AS_CHECK_BOX);
1164             mAction = action;
1165             if (checked) {
1166                 setChecked(checked);
1167             }
1168         }
1169 
1170         @Override
run()1171         public void run() {
1172             switch (mAction) {
1173                 case TOGGLE_CATEGORY:
1174                     mCategories = !mCategories;
1175                     refreshPalette();
1176                     break;
1177                 case TOGGLE_ALPHABETICAL:
1178                     mAlphabetical = !mAlphabetical;
1179                     refreshPalette();
1180                     break;
1181                 case TOGGLE_AUTO_CLOSE:
1182                     mAutoClose = !mAutoClose;
1183                     mAccordion.setAutoClose(mAutoClose);
1184                     break;
1185                 case REFRESH:
1186                     mPreviewIconFactory.refresh();
1187                     refreshPalette();
1188                     break;
1189                 case RESET:
1190                     mAlphabetical = false;
1191                     mCategories = true;
1192                     mAutoClose = true;
1193                     mPaletteMode = PaletteMode.SMALL_PREVIEW;
1194                     refreshPalette();
1195                     break;
1196             }
1197             savePaletteMode();
1198         }
1199     }
1200 
addMenu(Control control)1201     private void addMenu(Control control) {
1202         control.addMenuDetectListener(new MenuDetectListener() {
1203             @Override
1204             public void menuDetected(MenuDetectEvent e) {
1205                 showMenu(e.x, e.y);
1206             }
1207         });
1208     }
1209 
showMenu(int x, int y)1210     private void showMenu(int x, int y) {
1211         MenuManager manager = new MenuManager() {
1212             @Override
1213             public boolean isDynamic() {
1214                 return true;
1215             }
1216         };
1217         boolean previews = previewsAvailable();
1218         for (PaletteMode mode : PaletteMode.values()) {
1219             if (mode.isPreview() && !previews) {
1220                 continue;
1221             }
1222             manager.add(new PaletteModeAction(mode));
1223         }
1224         if (mPaletteMode.isPreview()) {
1225             manager.add(new Separator());
1226             manager.add(new ToggleViewOptionAction("Refresh Previews",
1227                     ToggleViewOptionAction.REFRESH,
1228                     false));
1229         }
1230         manager.add(new Separator());
1231         manager.add(new ToggleViewOptionAction("Show Categories",
1232                 ToggleViewOptionAction.TOGGLE_CATEGORY,
1233                 mCategories));
1234         manager.add(new ToggleViewOptionAction("Sort Alphabetically",
1235                 ToggleViewOptionAction.TOGGLE_ALPHABETICAL,
1236                 mAlphabetical));
1237         manager.add(new Separator());
1238         manager.add(new ToggleViewOptionAction("Auto Close Previous",
1239                 ToggleViewOptionAction.TOGGLE_AUTO_CLOSE,
1240                 mAutoClose));
1241         manager.add(new Separator());
1242         manager.add(new ToggleViewOptionAction("Reset",
1243                 ToggleViewOptionAction.RESET,
1244                 false));
1245 
1246         Menu menu = manager.createContextMenu(PaletteControl.this);
1247         menu.setLocation(x, y);
1248         menu.setVisible(true);
1249     }
1250 
1251     private final class ViewFinderListener implements CustomViewFinder.Listener {
1252         private final Composite mParent;
1253 
ViewFinderListener(Composite parent)1254         private ViewFinderListener(Composite parent) {
1255             mParent = parent;
1256         }
1257 
1258         @Override
viewsUpdated(Collection<String> customViews, Collection<String> thirdPartyViews)1259         public void viewsUpdated(Collection<String> customViews,
1260                 Collection<String> thirdPartyViews) {
1261             addCustomItems(mParent);
1262             mParent.layout(true);
1263         }
1264     }
1265 }
1266