/*
 * 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 com.android.ide.common.api.INode;
import com.android.ide.common.api.Point;
import com.android.ide.common.layout.LayoutConstants;
import com.android.ide.common.rendering.api.Capability;
import com.android.ide.common.rendering.api.RenderSession;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
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.ViewElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.sdklib.SdkConstants;

import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ActionContributionItem;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IContributionItem;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IStatusLineManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSource;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MenuDetectEvent;
import org.eclipse.swt.events.MenuDetectListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
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.ui.IActionBars;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.actions.ContributionItemFactory;
import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.internal.ide.IDEWorkbenchMessages;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
import org.w3c.dom.Node;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Displays the image rendered by the {@link GraphicalEditorPart} and handles
 * the interaction with the widgets.
 * <p/>
 * {@link LayoutCanvas} implements the "Canvas" control. The editor part
 * actually uses the {@link LayoutCanvasViewer}, which is a JFace viewer wrapper
 * around this control.
 * <p/>
 * The LayoutCanvas contains the painting logic for the canvas. Selection,
 * clipboard, view management etc. is handled in separate helper classes.
 *
 * @since GLE2
 */
@SuppressWarnings("restriction") // For WorkBench "Show In" support
public class LayoutCanvas extends Canvas {
    private final static QualifiedName NAME_ZOOM =
        new QualifiedName(AdtPlugin.PLUGIN_ID, "zoom");//$NON-NLS-1$

    private static final boolean DEBUG = false;

    /* package */ static final String PREFIX_CANVAS_ACTION = "canvas_action_";

    /** The layout editor that uses this layout canvas. */
    private final LayoutEditor mLayoutEditor;

    /** The Rules Engine, associated with the current project. */
    private RulesEngine mRulesEngine;

    /** GC wrapper given to the IViewRule methods. The GC itself is only defined in the
     *  context of {@link #onPaint(PaintEvent)}; otherwise it is null. */
    private GCWrapper mGCWrapper;

    /** Default font used on the canvas. Do not dispose, it's a system font. */
    private Font mFont;

    /** Current hover view info. Null when no mouse hover. */
    private CanvasViewInfo mHoverViewInfo;

    /** When true, always display the outline of all views. */
    private boolean mShowOutline;

    /** When true, display the outline of all empty parent views. */
    private boolean mShowInvisible;

    /** Drop target associated with this composite. */
    private DropTarget mDropTarget;

    /** Factory that can create {@link INode} proxies. */
    private final NodeFactory mNodeFactory = new NodeFactory();

    /** Vertical scaling & scrollbar information. */
    private CanvasTransform mVScale;

    /** Horizontal scaling & scrollbar information. */
    private CanvasTransform mHScale;

    /** Drag source associated with this canvas. */
    private DragSource mDragSource;

    /**
     * The current Outline Page, to set its model.
     * It isn't possible to call OutlinePage2.dispose() in this.dispose().
     * this.dispose() is called from GraphicalEditorPart.dispose(),
     * when page's widget is already disposed.
     * Added the DisposeListener to OutlinePage2 in order to correctly dispose this page.
     **/
    private OutlinePage mOutlinePage;

    /** Delete action for the Edit or context menu. */
    private Action mDeleteAction;

    /** Select-All action for the Edit or context menu. */
    private Action mSelectAllAction;

    /** Paste action for the Edit or context menu. */
    private Action mPasteAction;

    /** Cut action for the Edit or context menu. */
    private Action mCutAction;

    /** Copy action for the Edit or context menu. */
    private Action mCopyAction;

    /** Root of the context menu. */
    private MenuManager mMenuManager;

    /** The view hierarchy associated with this canvas. */
    private final ViewHierarchy mViewHierarchy = new ViewHierarchy(this);

    /** The selection in the canvas. */
    private final SelectionManager mSelectionManager = new SelectionManager(this);

    /** The overlay which paints the optional outline. */
    private OutlineOverlay mOutlineOverlay;

    /** The overlay which paints outlines around empty children */
    private EmptyViewsOverlay mEmptyOverlay;

    /** The overlay which paints the mouse hover. */
    private HoverOverlay mHoverOverlay;

    /** The overlay which paints the selection. */
    private SelectionOverlay mSelectionOverlay;

    /** The overlay which paints the rendered layout image. */
    private ImageOverlay mImageOverlay;

    /** The overlay which paints masks hiding everything but included content. */
    private IncludeOverlay mIncludeOverlay;

    /**
     * Gesture Manager responsible for identifying mouse, keyboard and drag and
     * drop events.
     */
    private final GestureManager mGestureManager = new GestureManager(this);

    /**
     * Native clipboard support.
     */
    private ClipboardSupport mClipboardSupport;

    public LayoutCanvas(LayoutEditor layoutEditor,
            RulesEngine rulesEngine,
            Composite parent,
            int style) {
        super(parent, style | SWT.DOUBLE_BUFFERED | SWT.V_SCROLL | SWT.H_SCROLL);
        mLayoutEditor = layoutEditor;
        mRulesEngine = rulesEngine;

        mClipboardSupport = new ClipboardSupport(this, parent);
        mHScale = new CanvasTransform(this, getHorizontalBar());
        mVScale = new CanvasTransform(this, getVerticalBar());

        // Unit test suite passes a null here; TODO: Replace with mocking
        IFile file = layoutEditor != null ? layoutEditor.getInputFile() : null;
        if (file != null) {
            String zoom = AdtPlugin.getFileProperty(file, NAME_ZOOM);
            if (zoom != null) {
                try {
                    double initialScale = Double.parseDouble(zoom);
                    if (initialScale > 0.0) {
                        mHScale.setScale(initialScale);
                        mVScale.setScale(initialScale);
                    }
                } catch (NumberFormatException nfe) {
                    // Ignore - use zoom=100%
                }
            }
        }

        mGCWrapper = new GCWrapper(mHScale, mVScale);

        Display display = getDisplay();
        mFont = display.getSystemFont();

        // --- Set up graphic overlays
        // mOutlineOverlay and mEmptyOverlay are initialized lazily
        mHoverOverlay = new HoverOverlay(mHScale, mVScale);
        mHoverOverlay.create(display);
        mSelectionOverlay = new SelectionOverlay(this);
        mSelectionOverlay.create(display);
        mImageOverlay = new ImageOverlay(this, mHScale, mVScale);
        mIncludeOverlay = new IncludeOverlay(this);
        mImageOverlay.create(display);

        // --- Set up listeners
        addPaintListener(new PaintListener() {
            public void paintControl(PaintEvent e) {
                onPaint(e);
            }
        });

        addControlListener(new ControlAdapter() {
            @Override
            public void controlResized(ControlEvent e) {
                super.controlResized(e);
                mHScale.setClientSize(getClientArea().width);
                mVScale.setClientSize(getClientArea().height);
            }
        });

        addKeyListener(new KeyListener() {

            public void keyPressed(KeyEvent e) {
                // Set up backspace as an alias for the delete action within the canvas.
                // On most Macs there is no delete key - though there IS a key labeled
                // "Delete" and it sends a backspace key code! In short, for Macs we should
                // treat backspace as delete, and it's harmless (and probably useful) to
                // handle backspace for other platforms as well.
                if (e.keyCode == SWT.BS) {
                    mDeleteAction.run();
                } else if (e.keyCode == SWT.ESC) {
                    mSelectionManager.selectParent();
                } else {
                    // Zooming actions
                    char c = e.character;
                    LayoutActionBar actionBar = mLayoutEditor.getGraphicalEditor()
                            .getLayoutActionBar();
                    if (c == '1' && actionBar.isZoomingAllowed()) {
                        setScale(1, true);
                    } else if (c == '0' && actionBar.isZoomingAllowed()) {
                        setFitScale(true);
                    } else if (e.keyCode == '0' && (e.stateMask & SWT.MOD2) != 0
                            && actionBar.isZoomingAllowed()) {
                        setFitScale(false);
                    } else if (c == '+' && actionBar.isZoomingAllowed()) {
                        actionBar.rescale(1);
                    } else if (c == '-' && actionBar.isZoomingAllowed()) {
                        actionBar.rescale(-1);
                    }
                }
            }

            public void keyReleased(KeyEvent e) {
            }
        });

        // --- setup drag'n'drop ---
        // DND Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html

        mDropTarget = createDropTarget(this);
        mDragSource = createDragSource(this);
        mGestureManager.registerListeners(mDragSource, mDropTarget);

        if (mLayoutEditor == null) {
            // TODO: In another CL we should use EasyMock/objgen to provide an editor.
            return; // Unit test
        }

        // --- setup context menu ---
        setupGlobalActionHandlers();
        createContextMenu();

        // --- setup outline ---
        // Get the outline associated with this editor, if any and of the right type.
        Object outline = layoutEditor.getAdapter(IContentOutlinePage.class);
        if (outline instanceof OutlinePage) {
            mOutlinePage = (OutlinePage) outline;
        }
    }

    @Override
    public void dispose() {
        super.dispose();

        mGestureManager.unregisterListeners(mDragSource, mDropTarget);

        if (mDropTarget != null) {
            mDropTarget.dispose();
            mDropTarget = null;
        }

        if (mRulesEngine != null) {
            mRulesEngine.dispose();
            mRulesEngine = null;
        }

        if (mDragSource != null) {
            mDragSource.dispose();
            mDragSource = null;
        }

        if (mClipboardSupport != null) {
            mClipboardSupport.dispose();
            mClipboardSupport = null;
        }

        if (mGCWrapper != null) {
            mGCWrapper.dispose();
            mGCWrapper = null;
        }

        if (mOutlineOverlay != null) {
            mOutlineOverlay.dispose();
            mOutlineOverlay = null;
        }

        if (mEmptyOverlay != null) {
            mEmptyOverlay.dispose();
            mEmptyOverlay = null;
        }

        if (mHoverOverlay != null) {
            mHoverOverlay.dispose();
            mHoverOverlay = null;
        }

        if (mSelectionOverlay != null) {
            mSelectionOverlay.dispose();
            mSelectionOverlay = null;
        }

        if (mImageOverlay != null) {
            mImageOverlay.dispose();
            mImageOverlay = null;
        }

        if (mIncludeOverlay != null) {
            mIncludeOverlay.dispose();
            mIncludeOverlay = null;
        }

        mViewHierarchy.dispose();
    }

    /** Returns the Rules Engine, associated with the current project. */
    /* package */ RulesEngine getRulesEngine() {
        return mRulesEngine;
    }

    /** Sets the Rules Engine, associated with the current project. */
    /* package */ void setRulesEngine(RulesEngine rulesEngine) {
        mRulesEngine = rulesEngine;
    }

    /**
     * Returns the factory to use to convert from {@link CanvasViewInfo} or from
     * {@link UiViewElementNode} to {@link INode} proxies.
     */
    /* package */ NodeFactory getNodeFactory() {
        return mNodeFactory;
    }

    /**
     * Returns the GCWrapper used to paint view rules.
     *
     * @return The GCWrapper used to paint view rules
     */
    /* package */ GCWrapper getGcWrapper() {
        return mGCWrapper;
    }

    /**
     * Returns the {@link LayoutEditor} associated with this canvas.
     */
    /* package */ LayoutEditor getLayoutEditor() {
        return mLayoutEditor;
    }

    /**
     * Returns the current {@link ImageOverlay} painting the rendered result
     *
     * @return the image overlay responsible for painting the rendered result, never null
     */
    ImageOverlay getImageOverlay() {
        return mImageOverlay;
    }

    /**
     * Returns the current {@link SelectionOverlay} painting the selection highlights
     *
     * @return the selection overlay responsible for painting the selection highlights,
     *         never null
     */
    SelectionOverlay getSelectionOverlay() {
        return mSelectionOverlay;
    }

    /**
     * Returns the current {@link HoverOverlay} painting the mouse hover.
     *
     * @return the hover overlay responsible for painting the mouse hover,
     *         never null
     */
    HoverOverlay getHoverOverlay() {
        return mHoverOverlay;
    }

    /**
     * Returns the horizontal {@link CanvasTransform} transform object, which can map
     * a layout point into a control point.
     *
     * @return A {@link CanvasTransform} for mapping between layout and control
     *         coordinates in the horizontal dimension.
     */
    /* package */ CanvasTransform getHorizontalTransform() {
        return mHScale;
    }

    /**
     * Returns the vertical {@link CanvasTransform} transform object, which can map a
     * layout point into a control point.
     *
     * @return A {@link CanvasTransform} for mapping between layout and control
     *         coordinates in the vertical dimension.
     */
    /* package */ CanvasTransform getVerticalTransform() {
        return mVScale;
    }

    /**
     * Returns the {@link OutlinePage} associated with this canvas
     *
     * @return the {@link OutlinePage} associated with this canvas
     */
    public OutlinePage getOutlinePage() {
        return mOutlinePage;
    }

    /**
     * Returns the {@link SelectionManager} associated with this canvas.
     *
     * @return The {@link SelectionManager} holding the selection for this
     *         canvas. Never null.
     */
    public SelectionManager getSelectionManager() {
        return mSelectionManager;
    }

    /**
     * Returns the {@link ViewHierarchy} object associated with this canvas,
     * holding the most recent rendered view of the scene, if valid.
     *
     * @return The {@link ViewHierarchy} object associated with this canvas.
     *         Never null.
     */
    public ViewHierarchy getViewHierarchy() {
        return mViewHierarchy;
    }

    /**
     * Returns the {@link ClipboardSupport} object associated with this canvas.
     *
     * @return The {@link ClipboardSupport} object for this canvas. Null only after dispose.
     */
    public ClipboardSupport getClipboardSupport() {
        return mClipboardSupport;
    }

    /** Returns the Select All action bound to this canvas */
    Action getSelectAllAction() {
        return mSelectAllAction;
    }

    /**
     * Sets the result of the layout rendering. The result object indicates if the layout
     * rendering succeeded. If it did, it contains a bitmap and the objects rectangles.
     *
     * Implementation detail: the bridge's computeLayout() method already returns a newly
     * allocated ILayourResult. That means we can keep this result and hold on to it
     * when it is valid.
     *
     * @param session The new scene, either valid or not.
     * @param explodedNodes The set of individual nodes the layout computer was asked to
     *            explode. Note that these are independent of the explode-all mode where
     *            all views are exploded; this is used only for the mode (
     *            {@link #showInvisibleViews(boolean)}) where individual invisible nodes
     *            are padded during certain interactions.
     */
    /* package */ void setSession(RenderSession session, Set<UiElementNode> explodedNodes) {
        // disable any hover
        clearHover();

        mViewHierarchy.setSession(session, explodedNodes);
        if (mViewHierarchy.isValid() && session != null) {
            Image image = mImageOverlay.setImage(session.getImage(), session.isAlphaChannelImage());

            mOutlinePage.setModel(mViewHierarchy.getRoot());

            if (image != null) {
                mHScale.setSize(image.getImageData().width, getClientArea().width);
                mVScale.setSize(image.getImageData().height, getClientArea().height);
            }
        }

        redraw();
    }

    /* package */ void setShowOutline(boolean newState) {
        mShowOutline = newState;
        redraw();
    }

    /* package */ double getScale() {
        return mHScale.getScale();
    }

    /* package */ void setScale(double scale, boolean redraw) {
        if (scale == getScale()) {
            return;
        }

        mHScale.setScale(scale);
        mVScale.setScale(scale);
        if (redraw) {
            redraw();
        }

        // Clear the zoom setting if it is almost identical to 1.0
        String zoomValue = (Math.abs(scale - 1.0) < 0.0001) ? null : Double.toString(scale);
        AdtPlugin.setFileProperty(mLayoutEditor.getInputFile(), NAME_ZOOM, zoomValue);
    }

    /**
     * Scales the canvas to best fit
     *
     * @param onlyZoomOut if true, then the zooming factor will never be larger than 1,
     *            which means that this function will zoom out if necessary to show the
     *            rendered image, but it will never zoom in.
     */
    void setFitScale(boolean onlyZoomOut) {
        Image image = getImageOverlay().getImage();
        if (image != null) {
            Rectangle canvasSize = getClientArea();
            int canvasWidth = canvasSize.width;
            int canvasHeight = canvasSize.height;

            ImageData imageData = image.getImageData();
            int sceneWidth = imageData.width;
            int sceneHeight = imageData.height;
            if (sceneWidth == 0.0 || sceneHeight == 0.0) {
                return;
            }

            // Reduce the margins if necessary
            int hDelta = canvasWidth - sceneWidth;
            int hMargin = 0;
            if (hDelta > 2 * CanvasTransform.DEFAULT_MARGIN) {
                hMargin = CanvasTransform.DEFAULT_MARGIN;
            } else if (hDelta > 0) {
                hMargin = hDelta / 2;
            }

            int vDelta = canvasHeight - sceneHeight;
            int vMargin = 0;
            if (vDelta > 2 * CanvasTransform.DEFAULT_MARGIN) {
                vMargin = CanvasTransform.DEFAULT_MARGIN;
            } else if (vDelta > 0) {
                vMargin = vDelta / 2;
            }

            double hScale = (canvasWidth - 2 * hMargin) / (double) sceneWidth;
            double vScale = (canvasHeight - 2 * vMargin) / (double) sceneHeight;

            double scale = Math.min(hScale, vScale);

            if (onlyZoomOut) {
                scale = Math.min(1.0, scale);
            }

            setScale(scale, true);
        }
    }

    /**
     * Transforms a point, expressed in layout coordinates, into "client" coordinates
     * relative to the control (and not relative to the display).
     *
     * @param canvasX X in the canvas coordinates
     * @param canvasY Y in the canvas coordinates
     * @return A new {@link Point} in control client coordinates (not display coordinates)
     */
    /* package */ Point layoutToControlPoint(int canvasX, int canvasY) {
        int x = mHScale.translate(canvasX);
        int y = mVScale.translate(canvasY);
        return new Point(x, y);
    }

    /**
     * Returns the action for the context menu corresponding to the given action id.
     * <p/>
     * For global actions such as copy or paste, the action id must be composed of
     * the {@link #PREFIX_CANVAS_ACTION} followed by one of {@link ActionFactory}'s
     * action ids.
     * <p/>
     * Returns null if there's no action for the given id.
     */
    /* package */ IAction getAction(String actionId) {
        String prefix = PREFIX_CANVAS_ACTION;
        if (mMenuManager == null ||
                actionId == null ||
                !actionId.startsWith(prefix)) {
            return null;
        }

        actionId = actionId.substring(prefix.length());

        for (IContributionItem contrib : mMenuManager.getItems()) {
            if (contrib instanceof ActionContributionItem &&
                    actionId.equals(contrib.getId())) {
                return ((ActionContributionItem) contrib).getAction();
            }
        }

        return null;
    }

    //---------------

    /**
     * Paints the canvas in response to paint events.
     */
    private void onPaint(PaintEvent e) {
        GC gc = e.gc;
        gc.setFont(mFont);
        mGCWrapper.setGC(gc);
        try {
            if (!mImageOverlay.isHiding()) {
                mImageOverlay.paint(gc);
            }

            if (mShowOutline) {
                if (mOutlineOverlay == null) {
                    mOutlineOverlay = new OutlineOverlay(mViewHierarchy, mHScale, mVScale);
                    mOutlineOverlay.create(getDisplay());
                }
                if (!mOutlineOverlay.isHiding()) {
                    mOutlineOverlay.paint(gc);
                }
            }

            if (mShowInvisible) {
                if (mEmptyOverlay == null) {
                    mEmptyOverlay = new EmptyViewsOverlay(mViewHierarchy, mHScale, mVScale);
                    mEmptyOverlay.create(getDisplay());
                }
                if (!mEmptyOverlay.isHiding()) {
                    mEmptyOverlay.paint(gc);
                }
            }

            if (!mHoverOverlay.isHiding()) {
                mHoverOverlay.paint(gc);
            }
            if (!mIncludeOverlay.isHiding()) {
                mIncludeOverlay.paint(gc);
            }

            if (!mSelectionOverlay.isHiding()) {
                mSelectionOverlay.paint(mSelectionManager, mGCWrapper, mRulesEngine);
            }
            mGestureManager.paint(gc);

        } finally {
            mGCWrapper.setGC(null);
        }
    }

    /**
     * Shows or hides invisible parent views, which are views which have empty bounds and
     * no children. The nodes which will be shown are provided by
     * {@link #getNodesToExplode()}.
     *
     * @param show When true, any invisible parent nodes are padded and highlighted
     *            ("exploded"), and when false any formerly exploded nodes are hidden.
     */
    /* package */ void showInvisibleViews(boolean show) {
        if (mShowInvisible == show) {
            return;
        }

        // Optimization: Avoid doing work when we don't have invisible parents (on show)
        // or formerly exploded nodes (on hide).
        if (show && !mViewHierarchy.hasInvisibleParents()) {
            return;
        } else if (!show && !mViewHierarchy.hasExplodedParents()) {
            return;
        }

        mShowInvisible = show;
        mLayoutEditor.recomputeLayout();
    }

    /**
     * Returns a set of nodes that should be exploded (forced non-zero padding during render),
     * or null if no nodes should be exploded. (Note that this is independent of the
     * explode-all mode, where all nodes are padded -- that facility does not use this
     * mechanism, which is only intended to be used to expose invisible parent nodes.
     *
     * @return The set of invisible parents, or null if no views should be expanded.
     */
    public Set<UiElementNode> getNodesToExplode() {
        if (mShowInvisible) {
            return mViewHierarchy.getInvisibleNodes();
        }

        // IF we have selection, and IF we have invisible nodes in the view,
        // see if any of the selected items are among the invisible nodes, and if so
        // add them to a lazily constructed set which we pass back for rendering.
        Set<UiElementNode> result = null;
        List<SelectionItem> selections = mSelectionManager.getSelections();
        if (selections.size() > 0) {
            List<CanvasViewInfo> invisibleParents = mViewHierarchy.getInvisibleViews();
            if (invisibleParents.size() > 0) {
                for (SelectionItem item : selections) {
                    CanvasViewInfo viewInfo = item.getViewInfo();
                    // O(n^2) here, but both the selection size and especially the
                    // invisibleParents size are expected to be small
                    if (invisibleParents.contains(viewInfo)) {
                        UiViewElementNode node = viewInfo.getUiViewNode();
                        if (node != null) {
                            if (result == null) {
                                result = new HashSet<UiElementNode>();
                            }
                            result.add(node);
                        }
                    }
                }
            }
        }

        return result;
    }

    /**
     * Clears the hover.
     */
    /* package */ void clearHover() {
        mHoverOverlay.clearHover();
    }

    /**
     * Hover on top of a known child.
     */
    /* package */ void hover(MouseEvent e) {
        // Check if a button is pressed; no hovers during drags
        if ((e.stateMask & SWT.BUTTON_MASK) != 0) {
            clearHover();
            return;
        }

        LayoutPoint p = ControlPoint.create(this, e).toLayout();
        CanvasViewInfo vi = mViewHierarchy.findViewInfoAt(p);

        // We don't hover on the root since it's not a widget per see and it is always there.
        if (vi != null && vi.isRoot()) {
            vi = null;
        }

        boolean needsUpdate = vi != mHoverViewInfo;
        mHoverViewInfo = vi;

        if (vi == null) {
            clearHover();
        } else {
            Rectangle r = vi.getSelectionRect();
            mHoverOverlay.setHover(r.x, r.y, r.width, r.height);
        }

        if (needsUpdate) {
            redraw();
        }
    }

    /**
     * Shows the given {@link CanvasViewInfo}, which can mean exposing its XML or if it's
     * an included element, its corresponding file.
     *
     * @param vi the {@link CanvasViewInfo} to be shown
     */
    public void show(CanvasViewInfo vi) {
        String url = vi.getIncludeUrl();
        if (url != null) {
            showInclude(url);
        } else {
            showXml(vi);
        }
    }

    /**
     * Shows the layout file referenced by the given url in the same project.
     *
     * @param url The layout attribute url of the form @layout/foo
     */
    private void showInclude(String url) {
        GraphicalEditorPart graphicalEditor = mLayoutEditor.getGraphicalEditor();
        IPath filePath = graphicalEditor.findResourceFile(url);

        IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();
        IPath workspacePath = workspace.getLocation();
        IEditorSite editorSite = graphicalEditor.getEditorSite();
        if (workspacePath.isPrefixOf(filePath)) {
            IPath relativePath = filePath.makeRelativeTo(workspacePath);
            IResource xmlFile = workspace.findMember(relativePath);
            if (xmlFile != null) {
                IFile leavingFile = graphicalEditor.getEditedFile();
                Reference next = Reference.create(graphicalEditor.getEditedFile());

                try {
                    IEditorPart openAlready = EditorUtility.isOpenInEditor(xmlFile);

                    // Show the included file as included within this click source?
                    if (openAlready != null) {
                        if (openAlready instanceof LayoutEditor) {
                            LayoutEditor editor = (LayoutEditor)openAlready;
                            GraphicalEditorPart gEditor = editor.getGraphicalEditor();
                            if (gEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) {
                                gEditor.showIn(next);
                            }
                        }
                    } else {
                        try {
                            // Set initial state of a new file
                            // TODO: Only set rendering target portion of the state
                            QualifiedName qname = ConfigurationComposite.NAME_CONFIG_STATE;
                            String state = AdtPlugin.getFileProperty(leavingFile, qname);
                            xmlFile.setSessionProperty(GraphicalEditorPart.NAME_INITIAL_STATE,
                                    state);
                        } catch (CoreException e) {
                            // pass
                        }

                        if (graphicalEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) {
                            try {
                                xmlFile.setSessionProperty(GraphicalEditorPart.NAME_INCLUDE, next);
                            } catch (CoreException e) {
                                // pass - worst that can happen is that we don't
                                //start with inclusion
                            }
                        }
                    }

                    EditorUtility.openInEditor(xmlFile, true);
                    return;
                } catch (PartInitException ex) {
                    AdtPlugin.log(ex, "Can't open %$1s", url); //$NON-NLS-1$
                }
            }
        } else {
            // It's not a path in the workspace; look externally
            // (this is probably an @android: path)
            if (filePath.isAbsolute()) {
                IFileStore fileStore = EFS.getLocalFileSystem().getStore(filePath);
                // fileStore = fileStore.getChild(names[i]);
                if (!fileStore.fetchInfo().isDirectory() && fileStore.fetchInfo().exists()) {
                    IWorkbenchPage page = editorSite.getWorkbenchWindow().getActivePage();
                    try {
                        IDE.openEditorOnFileStore(page, fileStore);
                        return;
                    } catch (PartInitException ex) {
                        AdtPlugin.log(ex, "Can't open %$1s", url); //$NON-NLS-1$
                    }
                }
            }
        }

        // Failed: display message to the user
        String message = String.format("Could not find resource %1$s", url);
        IStatusLineManager status = editorSite.getActionBars().getStatusLineManager();
        status.setErrorMessage(message);
        getDisplay().beep();
    }

    /**
     * Returns the layout resource name of this layout
     * @return
     */
    public String getLayoutResourceName() {
        GraphicalEditorPart graphicalEditor = mLayoutEditor.getGraphicalEditor();
        return graphicalEditor.getLayoutResourceName();
    }

    /**
     * Returns the layout resource url of the current layout
     *
     * @return
     */
    /*
    public String getMe() {
        GraphicalEditorPart graphicalEditor = mLayoutEditor.getGraphicalEditor();
        IFile editedFile = graphicalEditor.getEditedFile();
        return editedFile.getProjectRelativePath().toOSString();
    }
     */

    /**
     * Show the XML element corresponding to the given {@link CanvasViewInfo} (unless it's
     * a root).
     *
     * @param vi The clicked {@link CanvasViewInfo} whose underlying XML element we want
     *            to view
     */
    private void showXml(CanvasViewInfo vi) {
        // Warp to the text editor and show the corresponding XML for the
        // double-clicked widget
        if (vi.isRoot()) {
            return;
        }

        Node xmlNode = vi.getXmlNode();
        if (xmlNode != null) {
            boolean found = mLayoutEditor.show(xmlNode);
            if (!found) {
                getDisplay().beep();
            }
        }
    }

    //---------------

    /**
     * Helper to create the drag source for the given control.
     * <p/>
     * This is static with package-access so that {@link OutlinePage} can also
     * create an exact copy of the source with the same attributes.
     */
    /* package */static DragSource createDragSource(Control control) {
        DragSource source = new DragSource(control, DND.DROP_COPY | DND.DROP_MOVE);
        source.setTransfer(new Transfer[] {
                TextTransfer.getInstance(),
                SimpleXmlTransfer.getInstance()
        });
        return source;
    }

    /**
     * Helper to create the drop target for the given control.
     * <p/>
     * This is static with package-access so that {@link OutlinePage} can also
     * create an exact copy of the drop target with the same attributes.
     */
    /* package */static DropTarget createDropTarget(Control control) {
        DropTarget dropTarget = new DropTarget(
                control, DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_DEFAULT);
        dropTarget.setTransfer(new Transfer[] {
            SimpleXmlTransfer.getInstance()
        });
        return dropTarget;
    }

    //---------------

    /**
     * Invoked by the constructor to add our cut/copy/paste/delete/select-all
     * handlers in the global action handlers of this editor's site.
     * <p/>
     * This will enable the menu items under the global Edit menu and make them
     * invoke our actions as needed. As a benefit, the corresponding shortcut
     * accelerators will do what one would expect.
     */
    private void setupGlobalActionHandlers() {
        mCutAction = new Action() {
            @Override
            public void run() {
                mClipboardSupport.cutSelectionToClipboard(mSelectionManager.getSnapshot());
            }
        };

        copyActionAttributes(mCutAction, ActionFactory.CUT);

        mCopyAction = new Action() {
            @Override
            public void run() {
                mClipboardSupport.copySelectionToClipboard(mSelectionManager.getSnapshot());
            }
        };

        copyActionAttributes(mCopyAction, ActionFactory.COPY);

        mPasteAction = new Action() {
            @Override
            public void run() {
                mClipboardSupport.pasteSelection(mSelectionManager.getSnapshot());
            }
        };

        copyActionAttributes(mPasteAction, ActionFactory.PASTE);

        mDeleteAction = new Action() {
            @Override
            public void run() {
                mClipboardSupport.deleteSelection(
                        getDeleteLabel(),
                        mSelectionManager.getSnapshot());
            }
        };

        copyActionAttributes(mDeleteAction, ActionFactory.DELETE);

        mSelectAllAction = new Action() {
            @Override
            public void run() {
                GraphicalEditorPart graphicalEditor = getLayoutEditor().getGraphicalEditor();
                StyledText errorLabel = graphicalEditor.getErrorLabel();
                if (errorLabel.isFocusControl()) {
                    errorLabel.selectAll();
                    return;
                }

                mSelectionManager.selectAll();
            }
        };

        copyActionAttributes(mSelectAllAction, ActionFactory.SELECT_ALL);
    }

    /* package */ String getCutLabel() {
        return mCutAction.getText();
    }

    /* package */ String getDeleteLabel() {
        // verb "Delete" from the DELETE action's title
        return mDeleteAction.getText();
    }

    /**
     * Updates menu actions that depends on the selection.
     *
     * @param hasSelection True iff we have a non-empty selection
     */
    /* package */ void updateMenuActionState(boolean hasSelection) {
        StyledText errorLabel = mLayoutEditor.getGraphicalEditor().getErrorLabel();
        mCutAction.setEnabled(hasSelection);
        mCopyAction.setEnabled(hasSelection || errorLabel.getSelectionCount() > 0);
        mDeleteAction.setEnabled(hasSelection);
        mSelectAllAction.setEnabled(hasSelection || errorLabel.isFocusControl());

        // The paste operation is only available if we can paste our custom type.
        // We do not currently support pasting random text (e.g. XML). Maybe later.
        boolean hasSxt = mClipboardSupport.hasSxtOnClipboard();
        mPasteAction.setEnabled(hasSxt);
    }

    /**
     * Update the actions when this editor is activated
     *
     * @param bars the action bar for this canvas
     */
    public void updateGlobalActions(IActionBars bars) {
        bars.setGlobalActionHandler(ActionFactory.CUT.getId(), mCutAction);
        bars.setGlobalActionHandler(ActionFactory.COPY.getId(), mCopyAction);
        bars.setGlobalActionHandler(ActionFactory.PASTE.getId(), mPasteAction);
        bars.setGlobalActionHandler(ActionFactory.DELETE.getId(), mDeleteAction);
        bars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), mSelectAllAction);

        ITextEditor editor = mLayoutEditor.getStructuredTextEditor();
        IAction undoAction = editor.getAction(ActionFactory.UNDO.getId());
        bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), undoAction);
        IAction redoAction = editor.getAction(ActionFactory.REDO.getId());
        bars.setGlobalActionHandler(ActionFactory.REDO.getId(), redoAction);

        bars.updateActionBars();
    }

    /**
     * Helper for {@link #setupGlobalActionHandlers()}.
     * Copies the action attributes form the given {@link ActionFactory}'s action to
     * our action.
     * <p/>
     * {@link ActionFactory} provides access to the standard global actions in Eclipse.
     * <p/>
     * This allows us to grab the standard labels and icons for the
     * global actions such as copy, cut, paste, delete and select-all.
     */
    private void copyActionAttributes(Action action, ActionFactory factory) {
        IWorkbenchAction wa = factory.create(mLayoutEditor.getEditorSite().getWorkbenchWindow());
        action.setId(wa.getId());
        action.setText(wa.getText());
        action.setEnabled(wa.isEnabled());
        action.setDescription(wa.getDescription());
        action.setToolTipText(wa.getToolTipText());
        action.setAccelerator(wa.getAccelerator());
        action.setActionDefinitionId(wa.getActionDefinitionId());
        action.setImageDescriptor(wa.getImageDescriptor());
        action.setHoverImageDescriptor(wa.getHoverImageDescriptor());
        action.setDisabledImageDescriptor(wa.getDisabledImageDescriptor());
        action.setHelpListener(wa.getHelpListener());
    }

    /**
     * Creates the context menu for the canvas. This is called once from the canvas' constructor.
     * <p/>
     * The menu has a static part with actions that are always available such as
     * copy, cut, paste and show in > explorer. This is created by
     * {@link #setupStaticMenuActions(IMenuManager)}.
     * <p/>
     * There's also a dynamic part that is populated by the rules of the
     * selected elements, created by {@link DynamicContextMenu}.
     */
    private void createContextMenu() {

        // This manager is the root of the context menu.
        mMenuManager = new MenuManager() {
            @Override
            public boolean isDynamic() {
                return true;
            }
        };

        // Fill the menu manager with the static & dynamic actions
        setupStaticMenuActions(mMenuManager);
        new DynamicContextMenu(mLayoutEditor, this, mMenuManager);
        Menu menu = mMenuManager.createContextMenu(this);
        setMenu(menu);

        // Add listener to detect when the menu is about to be posted, such that
        // we can sync the selection. Without this, you can right click on something
        // in the canvas which is NOT selected, and the context menu will show items related
        // to the selection, NOT the item you clicked on!!
        addMenuDetectListener(new MenuDetectListener() {
            public void menuDetected(MenuDetectEvent e) {
                mSelectionManager.menuClick(e);
            }
        });
    }

    /**
     * Invoked by {@link #createContextMenu()} to create our *static* context menu once.
     * <p/>
     * The content of the menu itself does not change. However the state of the
     * various items is controlled by their associated actions.
     * <p/>
     * For cut/copy/paste/delete/select-all, we explicitly reuse the actions
     * created by {@link #setupGlobalActionHandlers()}, so this method must be
     * invoked after that one.
     */
    private void setupStaticMenuActions(IMenuManager manager) {
        manager.removeAll();

        manager.add(new SelectionManager.SelectionMenu(mLayoutEditor.getGraphicalEditor()));
        manager.add(new Separator());
        manager.add(mCutAction);
        manager.add(mCopyAction);
        manager.add(mPasteAction);
        manager.add(new Separator());
        manager.add(mDeleteAction);
        manager.add(new Separator());
        manager.add(new PlayAnimationMenu(this));
        manager.add(new Separator());

        // Group "Show Included In" and "Show In" together
        manager.add(new ShowWithinMenu(mLayoutEditor));

        // Create a "Show In" sub-menu and automatically populate it using standard
        // actions contributed by the workbench.
        String showInLabel = IDEWorkbenchMessages.Workbench_showIn;
        MenuManager showInSubMenu = new MenuManager(showInLabel);
        showInSubMenu.add(
                ContributionItemFactory.VIEWS_SHOW_IN.create(
                        mLayoutEditor.getSite().getWorkbenchWindow()));
        manager.add(showInSubMenu);
    }

    /**
     * Deletes the selection. Equivalent to pressing the Delete key.
     */
    /* package */ void delete() {
        mDeleteAction.run();
    }

    /**
     * Add new root in an existing empty XML layout.
     * <p/>
     * In case of error (unknown FQCN, document not empty), silently do nothing.
     * In case of success, the new element will have some default attributes set
     * (xmlns:android, layout_width and height). The edit is wrapped in a proper
     * undo.
     * <p/>
     * This is invoked by
     * {@link MoveGesture#drop(org.eclipse.swt.dnd.DropTargetEvent)}.
     *
     * @param rootFqcn A non-null non-empty FQCN that must match an existing
     *            {@link ViewElementDescriptor} to add as root to the current
     *            empty XML document.
     */
    /* package */ void createDocumentRoot(String rootFqcn) {

        // Need a valid empty document to create the new root
        final UiDocumentNode uiDoc = mLayoutEditor.getUiRootNode();
        if (uiDoc == null || uiDoc.getUiChildren().size() > 0) {
            debugPrintf("Failed to create document root for %1$s: document is not empty", rootFqcn);
            return;
        }

        // Find the view descriptor matching our FQCN
        final ViewElementDescriptor viewDesc = mLayoutEditor.getFqcnViewDescriptor(rootFqcn);
        if (viewDesc == null) {
            // TODO this could happen if dropping a custom view not known in this project
            debugPrintf("Failed to add document root, unknown FQCN %1$s", rootFqcn);
            return;
        }

        // Get the last segment of the FQCN for the undo title
        String title = rootFqcn;
        int pos = title.lastIndexOf('.');
        if (pos > 0 && pos < title.length() - 1) {
            title = title.substring(pos + 1);
        }
        title = String.format("Create root %1$s in document", title);

        mLayoutEditor.wrapUndoEditXmlModel(title, new Runnable() {
            public void run() {
                UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc);

                // A root node requires the Android XMLNS
                uiNew.setAttributeValue(
                        LayoutConstants.ANDROID_NS_PREFIX,
                        XmlnsAttributeDescriptor.XMLNS_URI,
                        SdkConstants.NS_RESOURCES,
                        true /*override*/);

                // Adjust the attributes
                DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);

                uiNew.createXmlNode();
            }
        });
    }

    private void debugPrintf(String message, Object... params) {
        if (DEBUG) {
            AdtPlugin.printToConsole("Canvas", String.format(message, params));
        }
    }
}
