/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.eclipse.org/org/documents/epl-v10.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ide.eclipse.adt.internal.editors.layout.gle2;

import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH;
import static com.android.ide.common.layout.LayoutConstants.ATTR_TEXT;
import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT;

import com.android.ide.common.api.InsertType;
import com.android.ide.common.api.Rect;
import com.android.ide.common.api.RuleAction.Toggle;
import com.android.ide.common.rendering.LayoutLibrary;
import com.android.ide.common.rendering.api.Capability;
import com.android.ide.common.rendering.api.LayoutLog;
import com.android.ide.common.rendering.api.RenderSession;
import com.android.ide.common.rendering.api.ViewInfo;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.IconFactory;
import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite;
import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService;
import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.PaletteMetadataDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository.RenderMode;
import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
import com.android.ide.eclipse.adt.internal.editors.ui.DecorComposite;
import com.android.ide.eclipse.adt.internal.editors.ui.IDecorContent;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.sdklib.IAndroidTarget;
import com.android.util.Pair;

import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CLabel;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSource;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.DragSourceListener;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.MenuDetectEvent;
import org.eclipse.swt.events.MenuDetectListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

/**
 * A palette control for the {@link GraphicalEditorPart}.
 * <p/>
 * The palette contains several groups, each with a UI name (e.g. layouts and views) and each
 * with a list of element descriptors.
 * <p/>
 *
 * TODO list:
 *   - The available items should depend on the actual GLE2 Canvas selection. Selected android
 *     views should force filtering on what they accept can be dropped on them (e.g. TabHost,
 *     TableLayout). Should enable/disable them, not hide them, to avoid shuffling around.
 *   - Optional: a text filter
 *   - Optional: have context-sensitive tools items, e.g. selection arrow tool,
 *     group selection tool, alignment, etc.
 */
public class PaletteControl extends Composite {

    /**
     * Wrapper to create a {@link PaletteControl} into a {@link DecorComposite}.
     */
    public static class PaletteDecor implements IDecorContent {
        private final GraphicalEditorPart mEditorPart;
        private PaletteControl mControl;

        public PaletteDecor(GraphicalEditorPart editor) {
            mEditorPart = editor;
        }

        public String getTitle() {
            return "Palette";
        }

        public Image getImage() {
            return IconFactory.getInstance().getIcon("editor_palette");  //$NON-NLS-1$
        }

        public void createControl(Composite parent) {
            mControl = new PaletteControl(parent, mEditorPart);
        }

        public Control getControl() {
            return mControl;
        }

        public void createToolbarItems(final ToolBar toolbar) {
            final ToolItem popupMenuItem = new ToolItem(toolbar, SWT.PUSH);
            popupMenuItem.setToolTipText("View Menu");
            popupMenuItem.setImage(IconFactory.getInstance().getIcon("view_menu"));
            popupMenuItem.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    Rectangle bounds = popupMenuItem.getBounds();
                    // Align menu horizontally with the toolbar button and
                    // vertically with the bottom of the toolbar
                    Point point = toolbar.toDisplay(bounds.x, bounds.y + bounds.height);
                    mControl.showMenu(point.x, point.y);
                }
            });
        }
    }

    /**
     * The parent grid layout that contains all the {@link Toggle} and
     * {@link IconTextItem} widgets.
     */
    private GraphicalEditorPart mEditor;
    private Color mBackground;
    private Color mForeground;

    /** The palette modes control various ways to visualize and lay out the views */
    private static enum PaletteMode {
        /** Show rendered previews of the views */
        PREVIEW("Show Previews", true),
        /** Show rendered previews of the views, scaled down to 75% */
        SMALL_PREVIEW("Show Small Previews", true),
        /** Show rendered previews of the views, scaled down to 50% */
        TINY_PREVIEW("Show Tiny Previews", true),
        /** Show an icon + text label */
        ICON_TEXT("Show Icon and Text", false),
        /** Show only icons, packed multiple per row */
        ICON_ONLY("Show Only Icons", true);

        PaletteMode(String actionLabel, boolean wrap) {
            mActionLabel = actionLabel;
            mWrap = wrap;
        }

        public String getActionLabel() {
            return mActionLabel;
        }

        public boolean getWrap() {
            return mWrap;
        }

        public boolean isPreview() {
            return this == PREVIEW || this == SMALL_PREVIEW || this == TINY_PREVIEW;
        }

        public boolean isScaledPreview() {
            return this == SMALL_PREVIEW || this == TINY_PREVIEW;
        }

        private final String mActionLabel;
        private final boolean mWrap;
    };

    /** Token used in preference string to record alphabetical sorting */
    private static final String VALUE_ALPHABETICAL = "alpha";   //$NON-NLS-1$
    /** Token used in preference string to record categories being turned off */
    private static final String VALUE_NO_CATEGORIES = "nocat"; //$NON-NLS-1$
    /** Token used in preference string to record auto close being turned off */
    private static final String VALUE_NO_AUTOCLOSE = "noauto";      //$NON-NLS-1$

    private final PreviewIconFactory mPreviewIconFactory = new PreviewIconFactory(this);
    private PaletteMode mPaletteMode = null;
    /** Use alphabetical sorting instead of natural order? */
    private boolean mAlphabetical;
    /** Use categories instead of a single large list of views? */
    private boolean mCategories = true;
    /** Auto-close the previous category when new categories are opened */
    private boolean mAutoClose = true;
    private AccordionControl mAccordion;
    private String mCurrentTheme;
    private String mCurrentDevice;
    private IAndroidTarget mCurrentTarget;
    private AndroidTargetData mCurrentTargetData;

    /**
     * Create the composite.
     * @param parent The parent composite.
     * @param editor An editor associated with this palette.
     */
    public PaletteControl(Composite parent, GraphicalEditorPart editor) {
        super(parent, SWT.NONE);

        mEditor = editor;
    }

    /** Reads UI mode from persistent store to preserve palette mode across IDE sessions */
    private void loadPaletteMode() {
        String paletteModes = AdtPrefs.getPrefs().getPaletteModes();
        if (paletteModes.length() > 0) {
            String[] tokens = paletteModes.split(","); //$NON-NLS-1$
            try {
                mPaletteMode = PaletteMode.valueOf(tokens[0]);
            } catch (Throwable t) {
                mPaletteMode = PaletteMode.values()[0];
            }
            mAlphabetical = paletteModes.contains(VALUE_ALPHABETICAL);
            mCategories = !paletteModes.contains(VALUE_NO_CATEGORIES);
            mAutoClose = !paletteModes.contains(VALUE_NO_AUTOCLOSE);
        } else {
            mPaletteMode = PaletteMode.SMALL_PREVIEW;
        }
    }

    /**
     * Returns the most recently stored version of auto-close-mode; this is the last
     * user-initiated setting of the auto-close mode (we programmatically switch modes when
     * you enter icons-only mode, and set it back to this when going to any other mode)
     */
    private boolean getSavedAutoCloseMode() {
        return !AdtPrefs.getPrefs().getPaletteModes().contains(VALUE_NO_AUTOCLOSE);
    }

    /** Saves UI mode to persistent store to preserve palette mode across IDE sessions */
    private void savePaletteMode() {
        StringBuilder sb = new StringBuilder();
        sb.append(mPaletteMode);
        if (mAlphabetical) {
            sb.append(',').append(VALUE_ALPHABETICAL);
        }
        if (!mCategories) {
            sb.append(',').append(VALUE_NO_CATEGORIES);
        }
        if (!mAutoClose) {
            sb.append(',').append(VALUE_NO_AUTOCLOSE);
        }
        AdtPrefs.getPrefs().setPaletteModes(sb.toString());
    }

    private void refreshPalette() {
        IAndroidTarget oldTarget = mCurrentTarget;
        mCurrentTarget = null;
        mCurrentTargetData = null;
        mCurrentTheme = null;
        mCurrentDevice = null;
        reloadPalette(oldTarget);
    }

    @Override
    protected void checkSubclass() {
        // Disable the check that prevents subclassing of SWT components
    }

    @Override
    public void dispose() {
        if (mBackground != null) {
            mBackground.dispose();
            mBackground = null;
        }
        if (mForeground != null) {
            mForeground.dispose();
            mForeground = null;
        }

        super.dispose();
    }

    /**
     * Returns the currently displayed target
     *
     * @return the current target, or null
     */
    public IAndroidTarget getCurrentTarget() {
        return mCurrentTarget;
    }

    /**
     * Returns the currently displayed theme (in palette modes that support previewing)
     *
     * @return the current theme, or null
     */
    public String getCurrentTheme() {
        return mCurrentTheme;
    }

    /**
     * Returns the currently displayed device (in palette modes that support previewing)
     *
     * @return the current device, or null
     */
    public String getCurrentDevice() {
        return mCurrentDevice;
    }

    /** Returns true if previews in the palette should be made available */
    private boolean previewsAvailable() {
        // Not layoutlib 5 -- we require custom background support to do
        // a decent job with previews
        LayoutLibrary layoutLibrary = mEditor.getLayoutLibrary();
        return layoutLibrary != null && layoutLibrary.supports(Capability.CUSTOM_BACKGROUND_COLOR);
    }

    /**
     * Loads or reloads the palette elements by using the layout and view descriptors from the
     * given target data.
     *
     * @param target The target that has just been loaded
     */
    public void reloadPalette(IAndroidTarget target) {
        ConfigurationComposite configuration = mEditor.getConfigurationComposite();
        String theme = configuration.getTheme();
        String device = configuration.getDevice();
        AndroidTargetData targetData =
            target != null ? Sdk.getCurrent().getTargetData(target) : null;
        if (target == mCurrentTarget && targetData == mCurrentTargetData
                && mCurrentTheme != null && mCurrentTheme.equals(theme)
                && mCurrentDevice != null && mCurrentDevice.equals(device)) {
            return;
        }
        mCurrentTheme = theme;
        mCurrentTarget = target;
        mCurrentTargetData = targetData;
        mCurrentDevice = device;
        mPreviewIconFactory.reset();

        if (targetData == null) {
            return;
        }

        Set<String> expandedCategories = null;
        if (mAccordion != null) {
            expandedCategories = mAccordion.getExpandedCategories();
            // We auto-expand all categories when showing icons-only. When returning to some
            // other mode we don't want to retain all categories open.
            if (expandedCategories.size() > 3) {
                expandedCategories = null;
            }
        }

        // Erase old content and recreate new
        for (Control c : getChildren()) {
            c.dispose();
        }

        if (mPaletteMode == null) {
            loadPaletteMode();
            assert mPaletteMode != null;
        }

        // Ensure that the palette mode is supported on this version of the layout library
        if (!previewsAvailable()) {
            if (mPaletteMode.isPreview()) {
                mPaletteMode = PaletteMode.ICON_TEXT;
            }
        }

        if (mPaletteMode.isPreview()) {
            if (mForeground != null) {
                mForeground.dispose();
                mForeground = null;
            }
            if (mBackground != null) {
                mBackground.dispose();
                mBackground = null;
            }
            RGB background = mPreviewIconFactory.getBackgroundColor();
            if (background != null) {
                mBackground = new Color(getDisplay(), background);
            }
            RGB foreground = mPreviewIconFactory.getForegroundColor();
            if (foreground != null) {
                mForeground = new Color(getDisplay(), foreground);
            }
        }

        List<String> headers = Collections.emptyList();
        final Map<String, List<ViewElementDescriptor>> categoryToItems;
        categoryToItems = new HashMap<String, List<ViewElementDescriptor>>();
        headers = new ArrayList<String>();
        List<Pair<String,List<ViewElementDescriptor>>> paletteEntries =
            ViewMetadataRepository.get().getPaletteEntries(targetData,
                    mAlphabetical, mCategories);
        for (Pair<String,List<ViewElementDescriptor>> pair : paletteEntries) {
            String category = pair.getFirst();
            List<ViewElementDescriptor> categoryItems = pair.getSecond();
            headers.add(category);
            categoryToItems.put(category, categoryItems);
        }

        headers.add("Custom & Library Views");

        // Set the categories to expand the first item if
        //   (1) we don't have a previously selected category, or
        //   (2) there's just one category anyway, or
        //   (3) the set of categories have changed so our previously selected category
        //       doesn't exist anymore (can happen when you toggle "Show Categories")
        if ((expandedCategories == null && headers.size() > 0) || headers.size() == 1 ||
                (expandedCategories != null && expandedCategories.size() >= 1
                        && !headers.contains(
                                expandedCategories.iterator().next().replace("&&", "&")))) { //$NON-NLS-1$ //$NON-NLS-2$
            // Expand the first category if we don't have a previous selection (e.g. refresh)
            expandedCategories = Collections.singleton(headers.get(0));
        }

        boolean wrap = mPaletteMode.getWrap();

        // Pack icon-only view vertically; others stretch to fill palette region
        boolean fillVertical = mPaletteMode != PaletteMode.ICON_ONLY;

        mAccordion = new AccordionControl(this, SWT.NONE, headers, fillVertical, wrap,
                expandedCategories) {
            @Override
            protected Composite createChildContainer(Composite parent, Object header, int style) {
                assert categoryToItems != null;
                List<ViewElementDescriptor> list = categoryToItems.get(header);
                final Composite composite;
                if (list == null) {
                    assert header.equals("Custom & Library Views");

                    Composite wrapper = new Composite(parent, SWT.NONE);
                    GridLayout gridLayout = new GridLayout(1, false);
                    gridLayout.marginWidth = gridLayout.marginHeight = 0;
                    gridLayout.horizontalSpacing = gridLayout.verticalSpacing = 0;
                    gridLayout.marginBottom = 3;
                    wrapper.setLayout(gridLayout);
                    if (mPaletteMode.isPreview() && mBackground != null) {
                        wrapper.setBackground(mBackground);
                    }
                    composite = super.createChildContainer(wrapper, header,
                            style | SWT.NO_BACKGROUND);
                    composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));

                    Button refreshButton = new Button(wrapper, SWT.PUSH | SWT.FLAT);
                    refreshButton.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER,
                            false, false, 1, 1));
                    refreshButton.setText("Refresh");
                    refreshButton.setImage(IconFactory.getInstance().getIcon("refresh")); //$NON-NLS-1$
                    refreshButton.addSelectionListener(new SelectionAdapter() {
                        @Override
                        public void widgetSelected(SelectionEvent e) {
                            CustomViewFinder finder = CustomViewFinder.get(mEditor.getProject());
                            finder.refresh(new ViewFinderListener(composite));
                        }
                    });

                    wrapper.layout(true);
                } else {
                    composite = super.createChildContainer(parent, header, style);
                    if (mPaletteMode.isPreview() && mBackground != null) {
                        composite.setBackground(mBackground);
                    }
                }
                addMenu(composite);
                return composite;
            }
            @Override
            protected void createChildren(Composite parent, Object header) {
                assert categoryToItems != null;
                List<ViewElementDescriptor> list = categoryToItems.get(header);
                if (list == null) {
                    assert header.equals("Custom & Library Views");
                    addCustomItems(parent);
                    return;
                } else {
                    for (ViewElementDescriptor desc : list) {
                        createItem(parent, desc);
                    }
                }
            }
        };
        addMenu(mAccordion);
        for (CLabel headerLabel : mAccordion.getHeaderLabels()) {
            addMenu(headerLabel);
        }
        setLayout(new FillLayout());

        // Expand All for icon-only mode, but don't store it as the persistent auto-close mode;
        // when we enter other modes it will read back whatever persistent mode.
        if (mPaletteMode == PaletteMode.ICON_ONLY) {
            mAccordion.expandAll(true);
            mAccordion.setAutoClose(false);
        } else {
            mAccordion.setAutoClose(getSavedAutoCloseMode());
        }

        layout(true);
    }

    protected void addCustomItems(final Composite parent) {
        final CustomViewFinder finder = CustomViewFinder.get(mEditor.getProject());
        Collection<String> allViews = finder.getAllViews();
        if (allViews == null) { // Not yet initialized: trigger an async refresh
            finder.refresh(new ViewFinderListener(parent));
            return;
        }

        // Remove previous content
        for (Control c : parent.getChildren()) {
            c.dispose();
        }

        // Add new views
        for (final String fqcn : allViews) {
            CustomViewDescriptorService service = CustomViewDescriptorService.getInstance();
            ViewElementDescriptor desc = service.getDescriptor(mEditor.getProject(), fqcn);
            if (desc == null) {
                // The descriptor lookup performs validation steps of the class, and may
                // in some cases determine that this is not a view and will return null;
                // guard against that.
                continue;
            }

            Control item = createItem(parent, desc);

            // Add control-click listener on custom view items to you can warp to
            // (and double click listener too -- the more discoverable, the better.)
            if (item instanceof IconTextItem) {
                IconTextItem it = (IconTextItem) item;
                it.addMouseListener(new MouseAdapter() {
                    @Override
                    public void mouseDoubleClick(MouseEvent e) {
                        AdtPlugin.openJavaClass(mEditor.getProject(), fqcn);
                    }

                    @Override
                    public void mouseDown(MouseEvent e) {
                        if ((e.stateMask & SWT.MOD1) != 0) {
                            AdtPlugin.openJavaClass(mEditor.getProject(), fqcn);
                        }
                    }
                });
            }
        }
    }

    /* package */ GraphicalEditorPart getEditor() {
        return mEditor;
    }

    private Control createItem(Composite parent, ViewElementDescriptor desc) {
        Control item = null;
        switch (mPaletteMode) {
            case SMALL_PREVIEW:
            case TINY_PREVIEW:
            case PREVIEW: {
                ImageDescriptor descriptor = mPreviewIconFactory.getImageDescriptor(desc);
                if (descriptor != null) {
                    Image image = descriptor.createImage();
                    ImageControl imageControl = new ImageControl(parent, SWT.None, image);
                    if (mPaletteMode.isScaledPreview()) {
                        // Try to preserve the overall size since rendering sizes typically
                        // vary with the dpi - so while the scaling factor for a 160 dpi
                        // rendering the scaling factor should be 0.5, for a 320 dpi one the
                        // scaling factor should be half that, 0.25.
                        float scale = 1.0f;
                        if (mPaletteMode == PaletteMode.SMALL_PREVIEW) {
                            scale = 0.75f;
                        } else if (mPaletteMode == PaletteMode.TINY_PREVIEW) {
                            scale = 0.5f;
                        }
                        int dpi = mEditor.getConfigurationComposite().getDensity().getDpiValue();
                        while (dpi > 160) {
                            scale = scale / 2;
                            dpi = dpi / 2;
                        }
                        imageControl.setScale(scale);
                    }
                    imageControl.setHoverColor(getDisplay().getSystemColor(SWT.COLOR_WHITE));
                    if (mBackground != null) {
                        imageControl.setBackground(mBackground);
                    }
                    String toolTip = desc.getUiName();
                    // It appears pretty much none of the descriptors have tooltips
                    //String descToolTip = desc.getTooltip();
                    //if (descToolTip != null && descToolTip.length() > 0) {
                    //    toolTip = toolTip + "\n" + descToolTip;
                    //}
                    imageControl.setToolTipText(toolTip);

                    item = imageControl;
                } else {
                    // Just use an Icon+Text item for these for now
                    item = new IconTextItem(parent, desc);
                    if (mForeground != null) {
                        item.setForeground(mForeground);
                        item.setBackground(mBackground);
                    }
                }
                break;
            }
            case ICON_TEXT: {
                item = new IconTextItem(parent, desc);
                break;
            }
            case ICON_ONLY: {
                item = new ImageControl(parent, SWT.None, desc.getGenericIcon());
                item.setToolTipText(desc.getUiName());
                break;
            }
            default:
                throw new IllegalArgumentException("Not yet implemented");
        }

        final DragSource source = new DragSource(item, DND.DROP_COPY);
        source.setTransfer(new Transfer[] { SimpleXmlTransfer.getInstance() });
        source.addDragListener(new DescDragSourceListener(desc));
        item.addDisposeListener(new DisposeListener() {
            public void widgetDisposed(DisposeEvent e) {
                source.dispose();
            }
        });
        addMenu(item);

        return item;
    }

    /**
     * An Item widget represents one {@link ElementDescriptor} that can be dropped on the
     * GLE2 canvas using drag'n'drop.
     */
    private static class IconTextItem extends CLabel implements MouseTrackListener {

        private boolean mMouseIn;

        public IconTextItem(Composite parent, ViewElementDescriptor desc) {
            super(parent, SWT.NONE);
            mMouseIn = false;

            setText(desc.getUiName());
            setImage(desc.getGenericIcon());
            setToolTipText(desc.getTooltip());
            addMouseTrackListener(this);
        }

        @Override
        public int getStyle() {
            int style = super.getStyle();
            if (mMouseIn) {
                style |= SWT.SHADOW_IN;
            }
            return style;
        }

        public void mouseEnter(MouseEvent e) {
            if (!mMouseIn) {
                mMouseIn = true;
                redraw();
            }
        }

        public void mouseExit(MouseEvent e) {
            if (mMouseIn) {
                mMouseIn = false;
                redraw();
            }
        }

        public void mouseHover(MouseEvent e) {
            // pass
        }
    }

    /**
     * A {@link DragSourceListener} that deals with drag'n'drop of
     * {@link ElementDescriptor}s.
     */
    private class DescDragSourceListener implements DragSourceListener {
        private final ViewElementDescriptor mDesc;
        private SimpleElement[] mElements;

        public DescDragSourceListener(ViewElementDescriptor desc) {
            mDesc = desc;
        }

        public void dragStart(DragSourceEvent e) {
            // See if we can find out the bounds of this element from a preview image.
            // Preview images are created before the drag source listener is notified
            // of the started drag.
            Rect bounds = null;
            Rect dragBounds = null;

            createDragImage(e);
            if (mImage != null && !mIsPlaceholder) {
                int width = mImageLayoutBounds.width;
                int height = mImageLayoutBounds.height;
                assert mImageLayoutBounds.x == 0;
                assert mImageLayoutBounds.y == 0;
                bounds = new Rect(0, 0, width, height);
                double scale = mEditor.getCanvasControl().getScale();
                int scaledWidth = (int) (scale * width);
                int scaledHeight = (int) (scale * height);
                int x = -scaledWidth / 2;
                int y = -scaledHeight / 2;
                dragBounds = new Rect(x, y, scaledWidth, scaledHeight);
            }

            SimpleElement se = new SimpleElement(
                    SimpleXmlTransfer.getFqcn(mDesc),
                    null   /* parentFqcn */,
                    bounds /* bounds */,
                    null   /* parentBounds */);
            if (mDesc instanceof PaletteMetadataDescriptor) {
                PaletteMetadataDescriptor pm = (PaletteMetadataDescriptor) mDesc;
                pm.initializeNew(se);
            }
            mElements = new SimpleElement[] { se };

            // Register this as the current dragged data
            GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance();
            dragInfo.startDrag(
                    mElements,
                    null /* selection */,
                    null /* canvas */,
                    null /* removeSource */);
            dragInfo.setDragBounds(dragBounds);
            dragInfo.setDragBaseline(mBaseline);


            e.doit = true;
        }

        public void dragSetData(DragSourceEvent e) {
            // Provide the data for the drop when requested by the other side.
            if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) {
                e.data = mElements;
            }
        }

        public void dragFinished(DragSourceEvent e) {
            // Unregister the dragged data.
            GlobalCanvasDragInfo.getInstance().stopDrag();
            mElements = null;
            if (mImage != null) {
                mImage.dispose();
                mImage = null;
            }
        }

        // TODO: Figure out the right dimensions to use for rendering.
        // We WILL crop this after rendering, but for performance reasons it would be good
        // not to make it much larger than necessary since to crop this we rely on
        // actually scanning pixels.

        /**
         * Width of the rendered preview image (before it is cropped), although the actual
         * width may be smaller (since we also take the device screen's size into account)
         */
        private static final int MAX_RENDER_HEIGHT = 400;

        /**
         * Height of the rendered preview image (before it is cropped), although the
         * actual width may be smaller (since we also take the device screen's size into
         * account)
         */
        private static final int MAX_RENDER_WIDTH = 500;

        /** Amount of alpha to multiply into the image (divided by 256) */
        private static final int IMG_ALPHA = 128;

        /** The image shown during the drag */
        private Image mImage;
        /** The non-effect bounds of the drag image */
        private Rectangle mImageLayoutBounds;
        private int mBaseline = -1;

        /**
         * If true, the image is a preview of the view, and if not it is a "fallback"
         * image of some sort, such as a rendering of the palette item itself
         */
        private boolean mIsPlaceholder;

        private void createDragImage(DragSourceEvent event) {
            mBaseline = -1;
            Pair<Image, Rectangle> preview = renderPreview();
            if (preview != null) {
                mImage = preview.getFirst();
                mImageLayoutBounds = preview.getSecond();
            } else {
                mImage = null;
                mImageLayoutBounds = null;
            }

            mIsPlaceholder = mImage == null;
            if (mIsPlaceholder) {
                // Couldn't render preview (or the preview is a blank image, such as for
                // example the preview of an empty layout), so instead create a placeholder
                // image
                // Render the palette item itself as an image
                Control control = ((DragSource) event.widget).getControl();
                GC gc = new GC(control);
                Point size = control.getSize();
                Display display = getDisplay();
                final Image image = new Image(display, size.x, size.y);
                gc.copyArea(image, 0, 0);
                gc.dispose();

                BufferedImage awtImage = SwtUtils.convertToAwt(image);
                if (awtImage != null) {
                    awtImage = ImageUtils.createDropShadow(awtImage, 3 /* shadowSize */,
                            0.7f /* shadowAlpha */, 0x000000 /* shadowRgb */);
                    mImage = SwtUtils.convertToSwt(display, awtImage, true, IMG_ALPHA);
                } else {
                    ImageData data = image.getImageData();
                    data.alpha = IMG_ALPHA;

                    // Changing the ImageData -after- constructing an image on it
                    // has no effect, so we have to construct a new image. Luckily these
                    // are tiny images.
                    mImage = new Image(display, data);
                }
                image.dispose();
            }

            event.image = mImage;

            if (!mIsPlaceholder) {
                // Shift the drag feedback image up such that it's centered under the
                // mouse pointer
                double scale = mEditor.getCanvasControl().getScale();
                event.offsetX = (int) (scale * mImageLayoutBounds.width / 2);
                event.offsetY = (int) (scale * mImageLayoutBounds.height / 2);
            }
        }

        /**
         * Performs the actual rendering of the descriptor into an image and returns the
         * image as well as the layout bounds of the image (not including drop shadow etc)
         */
        private Pair<Image, Rectangle> renderPreview() {
            ViewMetadataRepository repository = ViewMetadataRepository.get();
            RenderMode renderMode = repository.getRenderMode(mDesc.getFullClassName());
            if (renderMode == RenderMode.SKIP) {
                return null;
            }

            // Create blank XML document
            Document document = null;
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            try {
                factory.setNamespaceAware(true);
                factory.setValidating(false);
                DocumentBuilder builder = factory.newDocumentBuilder();
                document = builder.newDocument();
            } catch (ParserConfigurationException e) {
                return null;
            }

            // Insert our target view's XML into it as a node
            GraphicalEditorPart editor = getEditor();
            LayoutEditor layoutEditor = editor.getLayoutEditor();

            String viewName = mDesc.getXmlLocalName();
            Element element = document.createElement(viewName);

            // Set up a proper name space
            Attr attr = document.createAttributeNS(XmlnsAttributeDescriptor.XMLNS_URI,
                    "xmlns:android"); //$NON-NLS-1$
            attr.setValue(ANDROID_URI);
            element.getAttributes().setNamedItemNS(attr);

            element.setAttributeNS(ANDROID_URI,
                    ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT);
            element.setAttributeNS(ANDROID_URI,
                    ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);

            // This doesn't apply to all, but doesn't seem to cause harm and makes for a
            // better experience with text-oriented views like buttons and texts
            element.setAttributeNS(ANDROID_URI, ATTR_TEXT,
                    DescriptorsUtils.getBasename(mDesc.getUiName()));

            // Is this a palette variation?
            if (mDesc instanceof PaletteMetadataDescriptor) {
                PaletteMetadataDescriptor pm = (PaletteMetadataDescriptor) mDesc;
                pm.initializeNew(element);
            }

            document.appendChild(element);

            // Construct UI model from XML
            AndroidTargetData data = layoutEditor.getTargetData();
            DocumentDescriptor documentDescriptor;
            if (data == null) {
                documentDescriptor = new DocumentDescriptor("temp", null/*children*/);//$NON-NLS-1$
            } else {
                documentDescriptor = data.getLayoutDescriptors().getDescriptor();
            }
            UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode();
            model.setEditor(layoutEditor);
            model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider());
            model.loadFromXmlNode(document);

            // Call the create-hooks such that we for example insert mandatory
            // children into views like the DialerFilter, apply image source attributes
            // to ImageButtons, etc.
            LayoutCanvas canvas = editor.getCanvasControl();
            NodeFactory nodeFactory = canvas.getNodeFactory();
            UiElementNode parent = model.getUiRoot();
            UiElementNode child = parent.getUiChildren().get(0);
            if (child instanceof UiViewElementNode) {
                UiViewElementNode childUiNode = (UiViewElementNode) child;
                NodeProxy childNode = nodeFactory.create(childUiNode);

                // Applying create hooks as part of palette render should
                // not trigger model updates
                layoutEditor.setIgnoreXmlUpdate(true);
                try {
                    canvas.getRulesEngine().callCreateHooks(layoutEditor,
                            null, childNode, InsertType.CREATE_PREVIEW);
                } finally {
                    layoutEditor.setIgnoreXmlUpdate(false);
                }
            }

            Integer overrideBgColor = null;
            boolean hasTransparency = false;
            LayoutLibrary layoutLibrary = editor.getLayoutLibrary();
            if (layoutLibrary != null &&
                    layoutLibrary.supports(Capability.CUSTOM_BACKGROUND_COLOR)) {
                // It doesn't matter what the background color is as long as the alpha
                // is 0 (fully transparent). We're using red to make it more obvious if
                // for some reason the background is painted when it shouldn't be.
                overrideBgColor = new Integer(0x00FF0000);
            }

            RenderSession session = null;
            try {
                // Use at most the size of the screen for the preview render.
                // This is important since when we fill the size of certain views (like
                // a SeekBar), we want it to at most be the width of the screen, and for small
                // screens the RENDER_WIDTH was wider.
                Rect screenBounds = editor.getScreenBounds();
                int renderWidth = Math.min(screenBounds.w, MAX_RENDER_WIDTH);
                int renderHeight = Math.min(screenBounds.h, MAX_RENDER_HEIGHT);
                LayoutLog silentLogger = new LayoutLog();

                session = RenderService.create(editor)
                    .setModel(model)
                    .setSize(renderWidth, renderHeight)
                    .setLog(silentLogger)
                    .setOverrideBgColor(overrideBgColor)
                    .setDecorations(false)
                    .createRenderSession();
            } catch (Throwable t) {
                // Previews can fail for a variety of reasons -- let's not bug
                // the user with it
                return null;
            }

            if (session != null) {
                if (session.getResult().isSuccess()) {
                    BufferedImage image = session.getImage();
                    if (image != null) {
                        BufferedImage cropped;
                        Rect initialCrop = null;
                        ViewInfo viewInfo = null;

                        List<ViewInfo> viewInfoList = session.getRootViews();

                        if (viewInfoList != null && viewInfoList.size() > 0) {
                            viewInfo = viewInfoList.get(0);
                            mBaseline = viewInfo.getBaseLine();
                        }

                        if (viewInfo != null) {
                            int x1 = viewInfo.getLeft();
                            int x2 = viewInfo.getRight();
                            int y2 = viewInfo.getBottom();
                            int y1 = viewInfo.getTop();
                            initialCrop = new Rect(x1, y1, x2 - x1, y2 - y1);
                        }

                        if (hasTransparency) {
                            cropped = ImageUtils.cropBlank(image, initialCrop);
                        } else {
                            // Find out what the "background" color is such that we can properly
                            // crop it out of the image. To do this we pick out a pixel in the
                            // bottom right unpainted area. Rather than pick the one in the far
                            // bottom corner, we pick one as close to the bounds of the view as
                            // possible (but still outside of the bounds), such that we can
                            // deal with themes like the dialog theme.
                            int edgeX = image.getWidth() -1;
                            int edgeY = image.getHeight() -1;
                            if (viewInfo != null) {
                                if (viewInfo.getRight() < image.getWidth()-1) {
                                    edgeX = viewInfo.getRight()+1;
                                }
                                if (viewInfo.getBottom() < image.getHeight()-1) {
                                    edgeY = viewInfo.getBottom()+1;
                                }
                            }
                            int edgeColor = image.getRGB(edgeX, edgeY);
                            cropped = ImageUtils.cropColor(image, edgeColor, initialCrop);
                        }

                        if (cropped != null) {
                            int width = initialCrop != null ? initialCrop.w : cropped.getWidth();
                            int height = initialCrop != null ? initialCrop.h : cropped.getHeight();
                            boolean needsContrast = hasTransparency
                                    && !ImageUtils.containsDarkPixels(cropped);
                            cropped = ImageUtils.createDropShadow(cropped,
                                    hasTransparency ? 3 : 5 /* shadowSize */,
                                    !hasTransparency ? 0.6f : needsContrast ? 0.8f : 0.7f/*alpha*/,
                                    0x000000 /* shadowRgb */);

                            double scale = canvas.getScale();
                            if (scale != 1L) {
                                cropped = ImageUtils.scale(cropped, scale, scale);
                            }

                            Display display = getDisplay();
                            int alpha = (!hasTransparency || !needsContrast) ? IMG_ALPHA : -1;
                            Image swtImage = SwtUtils.convertToSwt(display, cropped, true, alpha);
                            Rectangle imageBounds = new Rectangle(0, 0, width, height);
                            return Pair.of(swtImage, imageBounds);
                        }
                    }
                }

                session.dispose();
            }

            return null;
        }

        /**
         * Utility method to print out the contents of the given XML document. This is
         * really useful when working on the preview code above. I'm including all the
         * code inside a constant false, which means the compiler will omit all the code,
         * but I'd like to leave it in the code base and by doing it this way rather than
         * as commented out code the code won't be accidentally broken.
         */
        @SuppressWarnings("all")
        private void dumpDocument(Document document) {
            // Diagnostics: print out the XML that we're about to render
            if (false) { // Will be omitted by the compiler
                org.apache.xml.serialize.OutputFormat outputFormat =
                    new org.apache.xml.serialize.OutputFormat(
                            "XML", "ISO-8859-1", true); //$NON-NLS-1$ //$NON-NLS-2$
                outputFormat.setIndent(2);
                outputFormat.setLineWidth(100);
                outputFormat.setIndenting(true);
                outputFormat.setOmitXMLDeclaration(true);
                outputFormat.setOmitDocumentType(true);
                StringWriter stringWriter = new StringWriter();
                // Using FQN here to avoid having an import above, which will result
                // in a deprecation warning, and there isn't a way to annotate a single
                // import element with a SuppressWarnings.
                org.apache.xml.serialize.XMLSerializer serializer =
                    new org.apache.xml.serialize.XMLSerializer(stringWriter, outputFormat);
                serializer.setNamespaces(true);
                try {
                    serializer.serialize(document.getDocumentElement());
                    System.out.println(stringWriter.toString());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /** Action for switching view modes via radio buttons */
    private class PaletteModeAction extends Action {
        private final PaletteMode mMode;

        PaletteModeAction(PaletteMode mode) {
            super(mode.getActionLabel(), IAction.AS_RADIO_BUTTON);
            mMode = mode;
            boolean selected = mMode == mPaletteMode;
            setChecked(selected);
            setEnabled(!selected);
        }

        @Override
        public void run() {
            if (isEnabled()) {
                mPaletteMode = mMode;
                refreshPalette();
                savePaletteMode();
            }
        }
    }

    /** Action for toggling various checkbox view modes - categories, sorting, etc */
    private class ToggleViewOptionAction extends Action {
        private final int mAction;
        final static int TOGGLE_CATEGORY = 1;
        final static int TOGGLE_ALPHABETICAL = 2;
        final static int TOGGLE_AUTO_CLOSE = 3;
        final static int REFRESH = 4;
        final static int RESET = 5;

        ToggleViewOptionAction(String title, int action, boolean checked) {
            super(title, (action == REFRESH || action == RESET) ? IAction.AS_PUSH_BUTTON
                    : IAction.AS_CHECK_BOX);
            mAction = action;
            if (checked) {
                setChecked(checked);
            }
        }

        @Override
        public void run() {
            switch (mAction) {
                case TOGGLE_CATEGORY:
                    mCategories = !mCategories;
                    refreshPalette();
                    break;
                case TOGGLE_ALPHABETICAL:
                    mAlphabetical = !mAlphabetical;
                    refreshPalette();
                    break;
                case TOGGLE_AUTO_CLOSE:
                    mAutoClose = !mAutoClose;
                    mAccordion.setAutoClose(mAutoClose);
                    break;
                case REFRESH:
                    mPreviewIconFactory.refresh();
                    refreshPalette();
                    break;
                case RESET:
                    mAlphabetical = false;
                    mCategories = true;
                    mAutoClose = true;
                    mPaletteMode = PaletteMode.SMALL_PREVIEW;
                    refreshPalette();
                    break;
            }
            savePaletteMode();
        }
    }

    private void addMenu(Control control) {
        control.addMenuDetectListener(new MenuDetectListener() {
            public void menuDetected(MenuDetectEvent e) {
                showMenu(e.x, e.y);
            }
        });
    }

    private void showMenu(int x, int y) {
        MenuManager manager = new MenuManager() {
            @Override
            public boolean isDynamic() {
                return true;
            }
        };
        boolean previews = previewsAvailable();
        for (PaletteMode mode : PaletteMode.values()) {
            if (mode.isPreview() && !previews) {
                continue;
            }
            manager.add(new PaletteModeAction(mode));
        }
        if (mPaletteMode.isPreview()) {
            manager.add(new Separator());
            manager.add(new ToggleViewOptionAction("Refresh Previews",
                    ToggleViewOptionAction.REFRESH,
                    false));
        }
        manager.add(new Separator());
        manager.add(new ToggleViewOptionAction("Show Categories",
                ToggleViewOptionAction.TOGGLE_CATEGORY,
                mCategories));
        manager.add(new ToggleViewOptionAction("Sort Alphabetically",
                ToggleViewOptionAction.TOGGLE_ALPHABETICAL,
                mAlphabetical));
        manager.add(new Separator());
        manager.add(new ToggleViewOptionAction("Auto Close Previous",
                ToggleViewOptionAction.TOGGLE_AUTO_CLOSE,
                mAutoClose));
        manager.add(new Separator());
        manager.add(new ToggleViewOptionAction("Reset",
                ToggleViewOptionAction.RESET,
                false));

        Menu menu = manager.createContextMenu(PaletteControl.this);
        menu.setLocation(x, y);
        menu.setVisible(true);
    }

    private final class ViewFinderListener implements CustomViewFinder.Listener {
        private final Composite mParent;

        private ViewFinderListener(Composite parent) {
            this.mParent = parent;
        }

        public void viewsUpdated(Collection<String> customViews,
                Collection<String> thirdPartyViews) {
            addCustomItems(mParent);
            mParent.layout(true);
        }
    }
}
