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
17package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
18
19import static com.android.SdkConstants.ANDROID_URI;
20import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
21import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
22import static com.android.SdkConstants.ATTR_TEXT;
23import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
24import static com.android.SdkConstants.XMLNS_ANDROID;
25import static com.android.SdkConstants.XMLNS_URI;
26
27import com.android.ide.common.api.InsertType;
28import com.android.ide.common.api.Rect;
29import com.android.ide.common.api.RuleAction.Toggle;
30import com.android.ide.common.rendering.LayoutLibrary;
31import com.android.ide.common.rendering.api.Capability;
32import com.android.ide.common.rendering.api.LayoutLog;
33import com.android.ide.common.rendering.api.RenderSession;
34import com.android.ide.common.rendering.api.ViewInfo;
35import com.android.ide.eclipse.adt.AdtPlugin;
36import com.android.ide.eclipse.adt.internal.editors.IconFactory;
37import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
38import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
39import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
40import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
41import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
42import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService;
43import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
44import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
45import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
46import com.android.ide.eclipse.adt.internal.editors.layout.gre.PaletteMetadataDescriptor;
47import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
48import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository.RenderMode;
49import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
50import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
51import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
52import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
53import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
54import com.android.ide.eclipse.adt.internal.sdk.Sdk;
55import com.android.sdklib.IAndroidTarget;
56import com.android.utils.Pair;
57
58import org.eclipse.jface.action.Action;
59import org.eclipse.jface.action.IAction;
60import org.eclipse.jface.action.IToolBarManager;
61import org.eclipse.jface.action.MenuManager;
62import org.eclipse.jface.action.Separator;
63import org.eclipse.jface.resource.ImageDescriptor;
64import org.eclipse.swt.SWT;
65import org.eclipse.swt.custom.CLabel;
66import org.eclipse.swt.dnd.DND;
67import org.eclipse.swt.dnd.DragSource;
68import org.eclipse.swt.dnd.DragSourceEvent;
69import org.eclipse.swt.dnd.DragSourceListener;
70import org.eclipse.swt.dnd.Transfer;
71import org.eclipse.swt.events.DisposeEvent;
72import org.eclipse.swt.events.DisposeListener;
73import org.eclipse.swt.events.MenuDetectEvent;
74import org.eclipse.swt.events.MenuDetectListener;
75import org.eclipse.swt.events.MouseAdapter;
76import org.eclipse.swt.events.MouseEvent;
77import org.eclipse.swt.events.MouseTrackListener;
78import org.eclipse.swt.events.SelectionAdapter;
79import org.eclipse.swt.events.SelectionEvent;
80import org.eclipse.swt.graphics.Color;
81import org.eclipse.swt.graphics.GC;
82import org.eclipse.swt.graphics.Image;
83import org.eclipse.swt.graphics.ImageData;
84import org.eclipse.swt.graphics.Point;
85import org.eclipse.swt.graphics.RGB;
86import org.eclipse.swt.graphics.Rectangle;
87import org.eclipse.swt.layout.FillLayout;
88import org.eclipse.swt.layout.GridData;
89import org.eclipse.swt.layout.GridLayout;
90import org.eclipse.swt.widgets.Button;
91import org.eclipse.swt.widgets.Composite;
92import org.eclipse.swt.widgets.Control;
93import org.eclipse.swt.widgets.Display;
94import org.eclipse.swt.widgets.Menu;
95import org.eclipse.swt.widgets.ToolBar;
96import org.eclipse.swt.widgets.ToolItem;
97import org.eclipse.wb.internal.core.editor.structure.IPage;
98import org.w3c.dom.Attr;
99import org.w3c.dom.Document;
100import org.w3c.dom.Element;
101
102import java.awt.image.BufferedImage;
103import java.io.IOException;
104import java.io.StringWriter;
105import java.util.ArrayList;
106import java.util.Collection;
107import java.util.Collections;
108import java.util.HashMap;
109import java.util.List;
110import java.util.Map;
111import 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 */
128public 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
137        PalettePage(GraphicalEditorPart editor) {
138            mEditorPart = editor;
139        }
140
141        @Override
142        public void createControl(Composite parent) {
143            mControl = new PaletteControl(parent, mEditorPart);
144        }
145
146        @Override
147        public Control getControl() {
148            return mControl;
149        }
150
151        @Override
152        public void dispose() {
153            mControl.dispose();
154        }
155
156        @Override
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         */
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
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
208        PaletteMode(String actionLabel, boolean wrap) {
209            mActionLabel = actionLabel;
210            mWrap = wrap;
211        }
212
213        public String getActionLabel() {
214            return mActionLabel;
215        }
216
217        public boolean getWrap() {
218            return mWrap;
219        }
220
221        public boolean isPreview() {
222            return this == PREVIEW || this == SMALL_PREVIEW || this == TINY_PREVIEW;
223        }
224
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     */
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 */
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     */
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 */
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
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
318    protected void checkSubclass() {
319        // Disable the check that prevents subclassing of SWT components
320    }
321
322    @Override
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     */
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     */
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     */
359    public String getCurrentDevice() {
360        return mCurrentDevice;
361    }
362
363    /** Returns true if previews in the palette should be made available */
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     */
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
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
609    /* package */ GraphicalEditorPart getEditor() {
610        return mEditor;
611    }
612
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
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
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
720        public void mouseEnter(MouseEvent e) {
721            if (!mMouseIn) {
722                mMouseIn = true;
723                redraw();
724            }
725        }
726
727        @Override
728        public void mouseExit(MouseEvent e) {
729            if (mMouseIn) {
730                mMouseIn = false;
731                redraw();
732            }
733        }
734
735        @Override
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
749        public DescDragSourceListener(ViewElementDescriptor desc) {
750            mDesc = desc;
751        }
752
753        @Override
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
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
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
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         */
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")
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
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
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
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
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
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
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
1254        private ViewFinderListener(Composite parent) {
1255            mParent = parent;
1256        }
1257
1258        @Override
1259        public void viewsUpdated(Collection<String> customViews,
1260                Collection<String> thirdPartyViews) {
1261            addCustomItems(mParent);
1262            mParent.layout(true);
1263        }
1264    }
1265}
1266