com.android.ide.eclipse.adt.internal.editors.layout.gle2.OutlinePage.java Source code

Java tutorial

Introduction

Here is the source code for com.android.ide.eclipse.adt.internal.editors.layout.gle2.OutlinePage.java

Source

/*
 * Copyright (C) 2010 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.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_COLUMN_COUNT;
import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN;
import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
import static com.android.SdkConstants.ATTR_LAYOUT_ROW;
import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN;
import static com.android.SdkConstants.ATTR_ROW_COUNT;
import static com.android.SdkConstants.ATTR_SRC;
import static com.android.SdkConstants.ATTR_TEXT;
import static com.android.SdkConstants.AUTO_URI;
import static com.android.SdkConstants.DRAWABLE_PREFIX;
import static com.android.SdkConstants.GRID_LAYOUT;
import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.URI_PREFIX;
import static org.eclipse.jface.viewers.StyledString.COUNTER_STYLER;
import static org.eclipse.jface.viewers.StyledString.QUALIFIER_STYLER;

import com.android.SdkConstants;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.api.INode;
import com.android.ide.common.api.InsertType;
import com.android.ide.common.layout.BaseLayoutRule;
import com.android.ide.common.layout.GridLayoutRule;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AdtUtils;
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.layout.LayoutEditorDelegate;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertySheetPage;
import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.utils.Pair;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
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.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.IElementComparer;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StyledCellLabelProvider;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.StyledString.Styler;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
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.MouseListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.INullSelectionListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.views.contentoutline.ContentOutlinePage;
import org.eclipse.wb.core.controls.SelfOrientingSashForm;
import org.eclipse.wb.internal.core.editor.structure.IPage;
import org.eclipse.wb.internal.core.editor.structure.PageSiteComposite;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

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

/**
 * An outline page for the layout canvas view.
 * <p/>
 * The page is created by {@link LayoutEditorDelegate#delegateGetAdapter(Class)}. This means
 * we have *one* instance of the outline page per open canvas editor.
 * <p/>
 * It sets itself as a listener on the site's selection service in order to be
 * notified of the canvas' selection changes.
 * The underlying page is also a selection provider (via IContentOutlinePage)
 * and as such it will broadcast selection changes to the site's selection service
 * (on which both the layout editor part and the property sheet page listen.)
 */
public class OutlinePage extends ContentOutlinePage implements INullSelectionListener, IPage {

    /** Label which separates outline text from additional attributes like text prefix or url */
    private static final String LABEL_SEPARATOR = " - ";

    /** Max character count in labels, used for truncation */
    private static final int LABEL_MAX_WIDTH = 50;

    /**
     * The graphical editor that created this outline.
     */
    private final GraphicalEditorPart mGraphicalEditorPart;

    /**
     * RootWrapper is a workaround: we can't set the input of the TreeView to its root
     * element, so we introduce a fake parent.
     */
    private final RootWrapper mRootWrapper = new RootWrapper();

    /**
     * Menu manager for the context menu actions.
     * The actions delegate to the current GraphicalEditorPart.
     */
    private MenuManager mMenuManager;

    private Composite mControl;
    private PropertySheetPage mPropertySheet;
    private PageSiteComposite mPropertySheetComposite;
    private boolean mShowPropertySheet;
    private boolean mShowHeader;
    private boolean mIgnoreSelection;
    private boolean mActive = true;

    /** Action to Select All in the tree */
    private final Action mTreeSelectAllAction = new Action() {
        @Override
        public void run() {
            getTreeViewer().getTree().selectAll();
            OutlinePage.this.fireSelectionChanged(getSelection());
        }

        @Override
        public String getId() {
            return ActionFactory.SELECT_ALL.getId();
        }
    };

    /** Action for moving items up in the tree */
    private Action mMoveUpAction = new Action("Move Up\t-", IconFactory.getInstance().getImageDescriptor("up")) { //$NON-NLS-2$

        @Override
        public String getId() {
            return "adt.outline.moveup"; //$NON-NLS-1$
        }

        @Override
        public boolean isEnabled() {
            return canMove(false);
        }

        @Override
        public void run() {
            move(false);
        }
    };

    /** Action for moving items down in the tree */
    private Action mMoveDownAction = new Action("Move Down\t+",
            IconFactory.getInstance().getImageDescriptor("down")) { //$NON-NLS-1$

        @Override
        public String getId() {
            return "adt.outline.movedown"; //$NON-NLS-1$
        }

        @Override
        public boolean isEnabled() {
            return canMove(true);
        }

        @Override
        public void run() {
            move(true);
        }
    };

    /**
     * Creates a new {@link OutlinePage} associated with the given editor
     *
     * @param graphicalEditorPart the editor associated with this outline
     */
    public OutlinePage(GraphicalEditorPart graphicalEditorPart) {
        super();
        mGraphicalEditorPart = graphicalEditorPart;
    }

    @Override
    public Control getControl() {
        // We've injected some controls between the root of the outline page
        // and the tree control, so return the actual root (a sash form) rather
        // than the superclass' implementation which returns the tree. If we don't
        // do this, various checks in the outline page which checks that getControl().getParent()
        // is the outline window itself will ignore this page.
        return mControl;
    }

    void setActive(boolean active) {
        if (active != mActive) {
            mActive = active;

            // Outlines are by default active when they are created; this is intended
            // for deactivating a hidden outline and later reactivating it
            assert mControl != null;
            if (active) {
                getSite().getPage().addSelectionListener(this);
                setModel(mGraphicalEditorPart.getCanvasControl().getViewHierarchy().getRoot());
            } else {
                getSite().getPage().removeSelectionListener(this);
                mRootWrapper.setRoot(null);
                if (mPropertySheet != null) {
                    mPropertySheet.selectionChanged(null, TreeSelection.EMPTY);
                }
            }
        }
    }

    /** Refresh all the icon state */
    public void refreshIcons() {
        TreeViewer treeViewer = getTreeViewer();
        if (treeViewer != null) {
            Tree tree = treeViewer.getTree();
            if (tree != null && !tree.isDisposed()) {
                treeViewer.refresh();
            }
        }
    }

    /**
     * Set whether the outline should be shown in the header
     *
     * @param show whether a header should be shown
     */
    public void setShowHeader(boolean show) {
        mShowHeader = show;
    }

    /**
     * Set whether the property sheet should be shown within this outline
     *
     * @param show whether the property sheet should show
     */
    public void setShowPropertySheet(boolean show) {
        if (show != mShowPropertySheet) {
            mShowPropertySheet = show;
            if (mControl == null) {
                return;
            }

            if (show && mPropertySheet == null) {
                createPropertySheet();
            } else if (!show) {
                mPropertySheetComposite.dispose();
                mPropertySheetComposite = null;
                mPropertySheet.dispose();
                mPropertySheet = null;
            }

            mControl.layout();
        }
    }

    @Override
    public void createControl(Composite parent) {
        mControl = new SelfOrientingSashForm(parent, SWT.VERTICAL);

        if (mShowHeader) {
            PageSiteComposite mOutlineComposite = new PageSiteComposite(mControl, SWT.BORDER);
            mOutlineComposite.setTitleText("Outline");
            mOutlineComposite.setTitleImage(IconFactory.getInstance().getIcon("components_view"));
            mOutlineComposite.setPage(new IPage() {
                @Override
                public void createControl(Composite outlineParent) {
                    createOutline(outlineParent);
                }

                @Override
                public void dispose() {
                }

                @Override
                public Control getControl() {
                    return getTreeViewer().getTree();
                }

                @Override
                public void setToolBar(IToolBarManager toolBarManager) {
                    makeContributions(null, toolBarManager, null);
                    toolBarManager.update(false);
                }

                @Override
                public void setFocus() {
                    getControl().setFocus();
                }
            });
        } else {
            createOutline(mControl);
        }

        if (mShowPropertySheet) {
            createPropertySheet();
        }
    }

    private void createOutline(Composite parent) {
        if (AdtUtils.isEclipse4()) {
            // This is a workaround for the focus behavior in Eclipse 4 where
            // the framework ends up calling setFocus() on the first widget in the outline
            // AFTER a mouse click has been received. Specifically, if the user clicks in
            // the embedded property sheet to for example give a Text property editor focus,
            // then after the mouse click, the Outline window activation event is processed,
            // and this event causes setFocus() to be called first on the PageBookView (which
            // ends up calling setFocus on the first control, normally the TreeViewer), and
            // then on the Page itself. We're dealing with the page setFocus() in the override
            // of that method in the class, such that it does nothing.
            // However, we have to also disable the setFocus on the first control in the
            // outline page. To deal with that, we create our *own* first control in the
            // outline, and make its setFocus() a no-op. We also make it invisible, since we
            // don't actually want anything but the tree viewer showing in the outline.
            Text text = new Text(parent, SWT.NONE) {
                @Override
                public boolean setFocus() {
                    // Focus no-op
                    return true;
                }

                @Override
                protected void checkSubclass() {
                    // Disable the check that prevents subclassing of SWT components
                }
            };
            text.setVisible(false);
        }

        super.createControl(parent);

        TreeViewer tv = getTreeViewer();
        tv.setAutoExpandLevel(2);
        tv.setContentProvider(new ContentProvider());
        tv.setLabelProvider(new LabelProvider());
        tv.setInput(mRootWrapper);
        tv.expandToLevel(mRootWrapper.getRoot(), 2);

        int supportedOperations = DND.DROP_COPY | DND.DROP_MOVE;
        Transfer[] transfers = new Transfer[] { SimpleXmlTransfer.getInstance() };

        tv.addDropSupport(supportedOperations, transfers, new OutlineDropListener(this, tv));
        tv.addDragSupport(supportedOperations, transfers, new OutlineDragListener(this, tv));

        // The tree viewer will hold CanvasViewInfo instances, however these
        // change each time the canvas is reloaded. OTOH layoutlib gives us
        // constant UiView keys which we can use to perform tree item comparisons.
        tv.setComparer(new IElementComparer() {
            @Override
            public int hashCode(Object element) {
                if (element instanceof CanvasViewInfo) {
                    UiViewElementNode key = ((CanvasViewInfo) element).getUiViewNode();
                    if (key != null) {
                        return key.hashCode();
                    }
                }
                if (element != null) {
                    return element.hashCode();
                }
                return 0;
            }

            @Override
            public boolean equals(Object a, Object b) {
                if (a instanceof CanvasViewInfo && b instanceof CanvasViewInfo) {
                    UiViewElementNode keyA = ((CanvasViewInfo) a).getUiViewNode();
                    UiViewElementNode keyB = ((CanvasViewInfo) b).getUiViewNode();
                    if (keyA != null) {
                        return keyA.equals(keyB);
                    }
                }
                if (a != null) {
                    return a.equals(b);
                }
                return false;
            }
        });
        tv.addDoubleClickListener(new IDoubleClickListener() {
            @Override
            public void doubleClick(DoubleClickEvent event) {
                // This used to open the property view, but now that properties are docked
                // let's use it for something else -- such as showing the editor source
                /*
                // Front properties panel; its selection is already linked
                IWorkbenchPage page = getSite().getPage();
                try {
                page.showView(IPageLayout.ID_PROP_SHEET, null, IWorkbenchPage.VIEW_ACTIVATE);
                } catch (PartInitException e) {
                AdtPlugin.log(e, "Could not activate property sheet");
                }
                */

                TreeItem[] selection = getTreeViewer().getTree().getSelection();
                if (selection.length > 0) {
                    CanvasViewInfo vi = getViewInfo(selection[0].getData());
                    if (vi != null) {
                        LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
                        canvas.show(vi);
                    }
                }
            }
        });

        setupContextMenu();

        // Listen to selection changes from the layout editor
        getSite().getPage().addSelectionListener(this);
        getControl().addDisposeListener(new DisposeListener() {

            @Override
            public void widgetDisposed(DisposeEvent e) {
                dispose();
            }
        });

        Tree tree = tv.getTree();
        tree.addKeyListener(new KeyListener() {

            @Override
            public void keyPressed(KeyEvent e) {
                if (e.character == '-') {
                    if (mMoveUpAction.isEnabled()) {
                        mMoveUpAction.run();
                    }
                } else if (e.character == '+') {
                    if (mMoveDownAction.isEnabled()) {
                        mMoveDownAction.run();
                    }
                }
            }

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

        setupTooltip();
    }

    /**
     * This flag is true when the mouse button is being pressed somewhere inside
     * the property sheet
     */
    private boolean mPressInPropSheet;

    private void createPropertySheet() {
        mPropertySheetComposite = new PageSiteComposite(mControl, SWT.BORDER);
        mPropertySheetComposite.setTitleText("Properties");
        mPropertySheetComposite.setTitleImage(IconFactory.getInstance().getIcon("properties_view"));
        mPropertySheet = new PropertySheetPage(mGraphicalEditorPart);
        mPropertySheetComposite.setPage(mPropertySheet);
        if (AdtUtils.isEclipse4()) {
            mPropertySheet.getControl().addMouseListener(new MouseListener() {
                @Override
                public void mouseDown(MouseEvent e) {
                    mPressInPropSheet = true;
                }

                @Override
                public void mouseUp(MouseEvent e) {
                    mPressInPropSheet = false;
                }

                @Override
                public void mouseDoubleClick(MouseEvent e) {
                }
            });
        }
    }

    @Override
    public void setFocus() {
        // Only call setFocus on the tree viewer if the mouse click isn't in the property
        // sheet area
        if (!mPressInPropSheet) {
            super.setFocus();
        }
    }

    @Override
    public void dispose() {
        mRootWrapper.setRoot(null);

        getSite().getPage().removeSelectionListener(this);
        super.dispose();
        if (mPropertySheet != null) {
            mPropertySheet.dispose();
            mPropertySheet = null;
        }
    }

    /**
     * Invoked by {@link LayoutCanvas} to set the model (a.k.a. the root view info).
     *
     * @param rootViewInfo The root of the view info hierarchy. Can be null.
     */
    public void setModel(CanvasViewInfo rootViewInfo) {
        if (!mActive) {
            return;
        }

        mRootWrapper.setRoot(rootViewInfo);

        TreeViewer tv = getTreeViewer();
        if (tv != null && !tv.getTree().isDisposed()) {
            Object[] expanded = tv.getExpandedElements();
            tv.refresh();
            tv.setExpandedElements(expanded);
            // Ensure that the root is expanded
            tv.expandToLevel(rootViewInfo, 2);
        }
    }

    /**
     * Returns the current tree viewer selection. Shouldn't be null,
     * although it can be {@link TreeSelection#EMPTY}.
     */
    @Override
    public ISelection getSelection() {
        return super.getSelection();
    }

    /**
     * Sets the outline selection.
     *
     * @param selection Only {@link ITreeSelection} will be used, otherwise the
     *   selection will be cleared (including a null selection).
     */
    @Override
    public void setSelection(ISelection selection) {
        // TreeViewer should be able to deal with a null selection, but let's make it safe
        if (selection == null) {
            selection = TreeSelection.EMPTY;
        }
        if (selection.equals(TreeSelection.EMPTY)) {
            return;
        }

        super.setSelection(selection);

        TreeViewer tv = getTreeViewer();
        if (tv == null || !(selection instanceof ITreeSelection) || selection.isEmpty()) {
            return;
        }

        // auto-reveal the selection
        ITreeSelection treeSel = (ITreeSelection) selection;
        for (TreePath p : treeSel.getPaths()) {
            tv.expandToLevel(p, 1);
        }
    }

    @Override
    protected void fireSelectionChanged(ISelection selection) {
        super.fireSelectionChanged(selection);
        if (mPropertySheet != null && !mIgnoreSelection) {
            mPropertySheet.selectionChanged(null, selection);
        }
    }

    /**
     * Listens to a workbench selection.
     * Only listen on selection coming from {@link LayoutEditorDelegate}, which avoid
     * picking up our own selections.
     */
    @Override
    public void selectionChanged(IWorkbenchPart part, ISelection selection) {
        if (mIgnoreSelection) {
            return;
        }

        if (part instanceof IEditorPart) {
            LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor((IEditorPart) part);
            if (delegate != null) {
                try {
                    mIgnoreSelection = true;
                    setSelection(selection);

                    if (mPropertySheet != null) {
                        mPropertySheet.selectionChanged(part, selection);
                    }
                } finally {
                    mIgnoreSelection = false;
                }
            }
        }
    }

    @Override
    public void selectionChanged(SelectionChangedEvent event) {
        if (!mIgnoreSelection) {
            super.selectionChanged(event);
        }
    }

    // ----

    /**
     * In theory, the root of the model should be the input of the {@link TreeViewer},
     * which would be the root {@link CanvasViewInfo}.
     * That means in theory {@link ContentProvider#getElements(Object)} should return
     * its own input as the single root node.
     * <p/>
     * However as described in JFace Bug 9262, this case is not properly handled by
     * a {@link TreeViewer} and leads to an infinite recursion in the tree viewer.
     * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=9262
     * <p/>
     * The solution is to wrap the tree viewer input in a dummy root node that acts
     * as a parent. This class does just that.
     */
    private static class RootWrapper {
        private CanvasViewInfo mRoot;

        public void setRoot(CanvasViewInfo root) {
            mRoot = root;
        }

        public CanvasViewInfo getRoot() {
            return mRoot;
        }
    }

    /** Return the {@link CanvasViewInfo} associated with the given TreeItem's data field */
    /* package */ static CanvasViewInfo getViewInfo(Object viewData) {
        if (viewData instanceof RootWrapper) {
            return ((RootWrapper) viewData).getRoot();
        }
        if (viewData instanceof CanvasViewInfo) {
            return (CanvasViewInfo) viewData;
        }
        return null;
    }

    // --- Content and Label Providers ---

    /**
     * Content provider for the Outline model.
     * Objects are going to be {@link CanvasViewInfo}.
     */
    private static class ContentProvider implements ITreeContentProvider {

        @Override
        public Object[] getChildren(Object element) {
            if (element instanceof RootWrapper) {
                CanvasViewInfo root = ((RootWrapper) element).getRoot();
                if (root != null) {
                    return new Object[] { root };
                }
            }
            if (element instanceof CanvasViewInfo) {
                List<CanvasViewInfo> children = ((CanvasViewInfo) element).getUniqueChildren();
                if (children != null) {
                    return children.toArray();
                }
            }
            return new Object[0];
        }

        @Override
        public Object getParent(Object element) {
            if (element instanceof CanvasViewInfo) {
                return ((CanvasViewInfo) element).getParent();
            }
            return null;
        }

        @Override
        public boolean hasChildren(Object element) {
            if (element instanceof CanvasViewInfo) {
                List<CanvasViewInfo> children = ((CanvasViewInfo) element).getChildren();
                if (children != null) {
                    return children.size() > 0;
                }
            }
            return false;
        }

        /**
         * Returns the root element.
         * Semantically, the root element is the single top-level XML element of the XML layout.
         */
        @Override
        public Object[] getElements(Object inputElement) {
            return getChildren(inputElement);
        }

        @Override
        public void dispose() {
            // pass
        }

        @Override
        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
            // pass
        }
    }

    /**
     * Label provider for the Outline model.
     * Objects are going to be {@link CanvasViewInfo}.
     */
    private class LabelProvider extends StyledCellLabelProvider {
        /**
         * Returns the element's logo with a fallback on the android logo.
         *
         * @param element the tree element
         * @return the image to be used as a logo
         */
        public Image getImage(Object element) {
            if (element instanceof CanvasViewInfo) {
                element = ((CanvasViewInfo) element).getUiViewNode();
            }

            if (element instanceof UiViewElementNode) {
                UiViewElementNode v = (UiViewElementNode) element;
                return v.getIcon();
            }

            return AdtPlugin.getAndroidLogo();
        }

        /**
         * Uses {@link UiElementNode#getStyledDescription} for the label for this tree item.
         */
        @Override
        public void update(ViewerCell cell) {
            Object element = cell.getElement();
            StyledString styledString = null;

            CanvasViewInfo vi = null;
            if (element instanceof CanvasViewInfo) {
                vi = (CanvasViewInfo) element;
                element = vi.getUiViewNode();
            }

            Image image = getImage(element);

            if (element instanceof UiElementNode) {
                UiElementNode node = (UiElementNode) element;
                styledString = node.getStyledDescription();
                Node xmlNode = node.getXmlNode();
                if (xmlNode instanceof Element) {
                    Element e = (Element) xmlNode;

                    // Temporary diagnostics code when developing GridLayout
                    if (GridLayoutRule.sDebugGridLayout) {

                        String namespace;
                        if (e.getNodeName().equals(GRID_LAYOUT) || e.getParentNode() != null
                                && e.getParentNode().getNodeName().equals(GRID_LAYOUT)) {
                            namespace = ANDROID_URI;
                        } else {
                            // Else: probably a v7 gridlayout
                            IProject project = mGraphicalEditorPart.getProject();
                            ProjectState projectState = Sdk.getProjectState(project);
                            if (projectState != null && projectState.isLibrary()) {
                                namespace = AUTO_URI;
                            } else {
                                ManifestInfo info = ManifestInfo.get(project);
                                namespace = URI_PREFIX + info.getPackage();
                            }
                        }

                        if (e.getNodeName() != null && e.getNodeName().endsWith(GRID_LAYOUT)) {
                            // Attach rowCount/columnCount info
                            String rowCount = e.getAttributeNS(namespace, ATTR_ROW_COUNT);
                            if (rowCount.length() == 0) {
                                rowCount = "?";
                            }
                            String columnCount = e.getAttributeNS(namespace, ATTR_COLUMN_COUNT);
                            if (columnCount.length() == 0) {
                                columnCount = "?";
                            }

                            styledString.append(" - columnCount=", QUALIFIER_STYLER);
                            styledString.append(columnCount, QUALIFIER_STYLER);
                            styledString.append(", rowCount=", QUALIFIER_STYLER);
                            styledString.append(rowCount, QUALIFIER_STYLER);
                        } else if (e.getParentNode() != null && e.getParentNode().getNodeName() != null
                                && e.getParentNode().getNodeName().endsWith(GRID_LAYOUT)) {
                            // Attach row/column info
                            String row = e.getAttributeNS(namespace, ATTR_LAYOUT_ROW);
                            if (row.length() == 0) {
                                row = "?";
                            }
                            Styler colStyle = QUALIFIER_STYLER;
                            String column = e.getAttributeNS(namespace, ATTR_LAYOUT_COLUMN);
                            if (column.length() == 0) {
                                column = "?";
                            } else {
                                String colCount = ((Element) e.getParentNode()).getAttributeNS(namespace,
                                        ATTR_COLUMN_COUNT);
                                if (colCount.length() > 0
                                        && Integer.parseInt(colCount) <= Integer.parseInt(column)) {
                                    colStyle = StyledString.createColorRegistryStyler(JFacePreferences.ERROR_COLOR,
                                            null);
                                }
                            }
                            String rowSpan = e.getAttributeNS(namespace, ATTR_LAYOUT_ROW_SPAN);
                            String columnSpan = e.getAttributeNS(namespace, ATTR_LAYOUT_COLUMN_SPAN);
                            if (rowSpan.length() == 0) {
                                rowSpan = "1";
                            }
                            if (columnSpan.length() == 0) {
                                columnSpan = "1";
                            }

                            styledString.append(" - cell (row=", QUALIFIER_STYLER);
                            styledString.append(row, QUALIFIER_STYLER);
                            styledString.append(',', QUALIFIER_STYLER);
                            styledString.append("col=", colStyle);
                            styledString.append(column, colStyle);
                            styledString.append(')', colStyle);
                            styledString.append(", span=(", QUALIFIER_STYLER);
                            styledString.append(columnSpan, QUALIFIER_STYLER);
                            styledString.append(',', QUALIFIER_STYLER);
                            styledString.append(rowSpan, QUALIFIER_STYLER);
                            styledString.append(')', QUALIFIER_STYLER);

                            String gravity = e.getAttributeNS(namespace, ATTR_LAYOUT_GRAVITY);
                            if (gravity != null && gravity.length() > 0) {
                                styledString.append(" : ", COUNTER_STYLER);
                                styledString.append(gravity, COUNTER_STYLER);
                            }

                        }
                    }

                    if (e.hasAttributeNS(ANDROID_URI, ATTR_TEXT)) {
                        // Show the text attribute
                        String text = e.getAttributeNS(ANDROID_URI, ATTR_TEXT);
                        if (text != null && text.length() > 0 && !text.contains(node.getDescriptor().getUiName())) {
                            if (text.charAt(0) == '@') {
                                String resolved = mGraphicalEditorPart.findString(text);
                                if (resolved != null) {
                                    text = resolved;
                                }
                            }
                            if (styledString.length() < LABEL_MAX_WIDTH - LABEL_SEPARATOR.length() - 2) {
                                styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);

                                styledString.append('"', QUALIFIER_STYLER);
                                styledString.append(truncate(text, styledString), QUALIFIER_STYLER);
                                styledString.append('"', QUALIFIER_STYLER);
                            }
                        }
                    } else if (e.hasAttributeNS(ANDROID_URI, ATTR_SRC)) {
                        // Show ImageView source attributes etc
                        String src = e.getAttributeNS(ANDROID_URI, ATTR_SRC);
                        if (src != null && src.length() > 0) {
                            if (src.startsWith(DRAWABLE_PREFIX)) {
                                src = src.substring(DRAWABLE_PREFIX.length());
                            }
                            styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
                            styledString.append(truncate(src, styledString), QUALIFIER_STYLER);
                        }
                    } else if (e.getTagName().equals(SdkConstants.VIEW_INCLUDE)) {
                        // Show the include reference.

                        // Note: the layout attribute is NOT in the Android namespace
                        String src = e.getAttribute(SdkConstants.ATTR_LAYOUT);
                        if (src != null && src.length() > 0) {
                            if (src.startsWith(LAYOUT_RESOURCE_PREFIX)) {
                                src = src.substring(LAYOUT_RESOURCE_PREFIX.length());
                            }
                            styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
                            styledString.append(truncate(src, styledString), QUALIFIER_STYLER);
                        }
                    }
                }
            } else if (element == null && vi != null) {
                // It's an inclusion-context: display it
                Reference includedWithin = mGraphicalEditorPart.getIncludedWithin();
                if (includedWithin != null) {
                    styledString = new StyledString();
                    styledString.append(includedWithin.getDisplayName(), QUALIFIER_STYLER);
                    image = IconFactory.getInstance().getIcon(SdkConstants.VIEW_INCLUDE);
                }
            }

            if (styledString == null) {
                styledString = new StyledString();
                styledString.append(element == null ? "(null)" : element.toString());
            }

            cell.setText(styledString.toString());
            cell.setStyleRanges(styledString.getStyleRanges());
            cell.setImage(image);
            super.update(cell);
        }

        @Override
        public boolean isLabelProperty(Object element, String property) {
            return super.isLabelProperty(element, property);
        }
    }

    // --- Context Menu ---

    /**
     * This viewer uses its own actions that delegate to the ones given
     * by the {@link LayoutCanvas}. All the processing is actually handled
     * directly by the canvas and this viewer only gets refreshed as a
     * consequence of the canvas changing the XML model.
     */
    private void setupContextMenu() {

        mMenuManager = new MenuManager();
        mMenuManager.removeAll();

        mMenuManager.add(mMoveUpAction);
        mMenuManager.add(mMoveDownAction);
        mMenuManager.add(new Separator());

        mMenuManager.add(new SelectionManager.SelectionMenu(mGraphicalEditorPart));
        mMenuManager.add(new Separator());
        final String prefix = LayoutCanvas.PREFIX_CANVAS_ACTION;
        mMenuManager.add(new DelegateAction(prefix + ActionFactory.CUT.getId()));
        mMenuManager.add(new DelegateAction(prefix + ActionFactory.COPY.getId()));
        mMenuManager.add(new DelegateAction(prefix + ActionFactory.PASTE.getId()));

        mMenuManager.add(new Separator());

        mMenuManager.add(new DelegateAction(prefix + ActionFactory.DELETE.getId()));

        mMenuManager.addMenuListener(new IMenuListener() {
            @Override
            public void menuAboutToShow(IMenuManager manager) {
                // Update all actions to match their LayoutCanvas counterparts
                for (IContributionItem contrib : manager.getItems()) {
                    if (contrib instanceof ActionContributionItem) {
                        IAction action = ((ActionContributionItem) contrib).getAction();
                        if (action instanceof DelegateAction) {
                            ((DelegateAction) action).updateFromEditorPart(mGraphicalEditorPart);
                        }
                    }
                }
            }
        });

        new DynamicContextMenu(mGraphicalEditorPart.getEditorDelegate(), mGraphicalEditorPart.getCanvasControl(),
                mMenuManager);

        getTreeViewer().getTree().setMenu(mMenuManager.createContextMenu(getControl()));

        // Update Move Up/Move Down state only when the menu is opened
        getTreeViewer().getTree().addMenuDetectListener(new MenuDetectListener() {
            @Override
            public void menuDetected(MenuDetectEvent e) {
                mMenuManager.update(IAction.ENABLED);
            }
        });
    }

    /**
     * An action that delegates its properties and behavior to a target action.
     * The target action can be null or it can change overtime, typically as the
     * layout canvas' editor part is activated or closed.
     */
    private static class DelegateAction extends Action {
        private IAction mTargetAction;
        private final String mCanvasActionId;

        public DelegateAction(String canvasActionId) {
            super(canvasActionId);
            setId(canvasActionId);
            mCanvasActionId = canvasActionId;
        }

        // --- Methods form IAction ---

        /** Returns the target action's {@link #isEnabled()} if defined, or false. */
        @Override
        public boolean isEnabled() {
            return mTargetAction == null ? false : mTargetAction.isEnabled();
        }

        /** Returns the target action's {@link #isChecked()} if defined, or false. */
        @Override
        public boolean isChecked() {
            return mTargetAction == null ? false : mTargetAction.isChecked();
        }

        /** Returns the target action's {@link #isHandled()} if defined, or false. */
        @Override
        public boolean isHandled() {
            return mTargetAction == null ? false : mTargetAction.isHandled();
        }

        /** Runs the target action if defined. */
        @Override
        public void run() {
            if (mTargetAction != null) {
                mTargetAction.run();
            }
            super.run();
        }

        /**
         * Updates this action to delegate to its counterpart in the given editor part
         *
         * @param editorPart The editor being updated
         */
        public void updateFromEditorPart(GraphicalEditorPart editorPart) {
            LayoutCanvas canvas = editorPart == null ? null : editorPart.getCanvasControl();
            if (canvas == null) {
                mTargetAction = null;
            } else {
                mTargetAction = canvas.getAction(mCanvasActionId);
            }

            if (mTargetAction != null) {
                setText(mTargetAction.getText());
                setId(mTargetAction.getId());
                setDescription(mTargetAction.getDescription());
                setImageDescriptor(mTargetAction.getImageDescriptor());
                setHoverImageDescriptor(mTargetAction.getHoverImageDescriptor());
                setDisabledImageDescriptor(mTargetAction.getDisabledImageDescriptor());
                setToolTipText(mTargetAction.getToolTipText());
                setActionDefinitionId(mTargetAction.getActionDefinitionId());
                setHelpListener(mTargetAction.getHelpListener());
                setAccelerator(mTargetAction.getAccelerator());
                setChecked(mTargetAction.isChecked());
                setEnabled(mTargetAction.isEnabled());
            } else {
                setEnabled(false);
            }
        }
    }

    /** Returns the associated editor with this outline */
    /* package */GraphicalEditorPart getEditor() {
        return mGraphicalEditorPart;
    }

    @Override
    public void setActionBars(IActionBars actionBars) {
        super.setActionBars(actionBars);

        // Map Outline actions to canvas actions such that they share Undo context etc
        LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
        canvas.updateGlobalActions(actionBars);

        // Special handling for Select All since it's different than the canvas (will
        // include selecting the root etc)
        actionBars.setGlobalActionHandler(mTreeSelectAllAction.getId(), mTreeSelectAllAction);
        actionBars.updateActionBars();
    }

    // ---- Move Up/Down Support ----

    /** Returns true if the current selected item can be moved */
    private boolean canMove(boolean forward) {
        CanvasViewInfo viewInfo = getSingleSelectedItem();
        if (viewInfo != null) {
            UiViewElementNode node = viewInfo.getUiViewNode();
            if (forward) {
                return findNext(node) != null;
            } else {
                return findPrevious(node) != null;
            }
        }

        return false;
    }

    /** Moves the current selected item down (forward) or up (not forward) */
    private void move(boolean forward) {
        CanvasViewInfo viewInfo = getSingleSelectedItem();
        if (viewInfo != null) {
            final Pair<UiViewElementNode, Integer> target;
            UiViewElementNode selected = viewInfo.getUiViewNode();
            if (forward) {
                target = findNext(selected);
            } else {
                target = findPrevious(selected);
            }
            if (target != null) {
                final LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
                final SelectionManager selectionManager = canvas.getSelectionManager();
                final ArrayList<SelectionItem> dragSelection = new ArrayList<SelectionItem>();
                dragSelection.add(selectionManager.createSelection(viewInfo));
                SelectionManager.sanitize(dragSelection);

                if (!dragSelection.isEmpty()) {
                    final SimpleElement[] elements = SelectionItem.getAsElements(dragSelection);
                    UiViewElementNode parentNode = target.getFirst();
                    final NodeProxy targetNode = canvas.getNodeFactory().create(parentNode);

                    // Record children of the target right before the drop (such that we
                    // can find out after the drop which exact children were inserted)
                    Set<INode> children = new HashSet<INode>();
                    for (INode node : targetNode.getChildren()) {
                        children.add(node);
                    }

                    String label = MoveGesture.computeUndoLabel(targetNode, elements, DND.DROP_MOVE);
                    canvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(label, new Runnable() {
                        @Override
                        public void run() {
                            InsertType insertType = InsertType.MOVE_INTO;
                            if (dragSelection.get(0).getNode().getParent() == targetNode) {
                                insertType = InsertType.MOVE_WITHIN;
                            }
                            canvas.getRulesEngine().setInsertType(insertType);
                            int index = target.getSecond();
                            BaseLayoutRule.insertAt(targetNode, elements, false, index);
                            targetNode.applyPendingChanges();
                            canvas.getClipboardSupport().deleteSelection("Remove", dragSelection);
                        }
                    });

                    // Now find out which nodes were added, and look up their
                    // corresponding CanvasViewInfos
                    final List<INode> added = new ArrayList<INode>();
                    for (INode node : targetNode.getChildren()) {
                        if (!children.contains(node)) {
                            added.add(node);
                        }
                    }

                    selectionManager.setOutlineSelection(added);
                }
            }
        }
    }

    /**
     * Returns the {@link CanvasViewInfo} for the currently selected item, or null if
     * there are no or multiple selected items
     *
     * @return the current selected item if there is exactly one item selected
     */
    private CanvasViewInfo getSingleSelectedItem() {
        TreeItem[] selection = getTreeViewer().getTree().getSelection();
        if (selection.length == 1) {
            return getViewInfo(selection[0].getData());
        }

        return null;
    }

    /** Returns the pair [parent,index] of the next node (when iterating forward) */
    @VisibleForTesting
    /* package */ static Pair<UiViewElementNode, Integer> findNext(UiViewElementNode node) {
        UiElementNode parent = node.getUiParent();
        if (parent == null) {
            return null;
        }

        UiElementNode next = node.getUiNextSibling();
        if (next != null) {
            if (DescriptorsUtils.canInsertChildren(next.getDescriptor(), null)) {
                return getFirstPosition(next);
            } else {
                return getPositionAfter(next);
            }
        }

        next = parent.getUiNextSibling();
        if (next != null) {
            return getPositionBefore(next);
        } else {
            UiElementNode grandParent = parent.getUiParent();
            if (grandParent != null) {
                return getLastPosition(grandParent);
            }
        }

        return null;
    }

    /** Returns the pair [parent,index] of the previous node (when iterating backward) */
    @VisibleForTesting
    /* package */ static Pair<UiViewElementNode, Integer> findPrevious(UiViewElementNode node) {
        UiElementNode prev = node.getUiPreviousSibling();
        if (prev != null) {
            UiElementNode curr = prev;
            while (true) {
                List<UiElementNode> children = curr.getUiChildren();
                if (children.size() > 0) {
                    curr = children.get(children.size() - 1);
                    continue;
                }
                if (DescriptorsUtils.canInsertChildren(curr.getDescriptor(), null)) {
                    return getFirstPosition(curr);
                } else {
                    if (curr == prev) {
                        return getPositionBefore(curr);
                    } else {
                        return getPositionAfter(curr);
                    }
                }
            }
        }

        return getPositionBefore(node.getUiParent());
    }

    /** Returns the pair [parent,index] of the position immediately before the given node  */
    private static Pair<UiViewElementNode, Integer> getPositionBefore(UiElementNode node) {
        if (node != null) {
            UiElementNode parent = node.getUiParent();
            if (parent != null && parent instanceof UiViewElementNode) {
                return Pair.of((UiViewElementNode) parent, node.getUiSiblingIndex());
            }
        }

        return null;
    }

    /** Returns the pair [parent,index] of the position immediately following the given node  */
    private static Pair<UiViewElementNode, Integer> getPositionAfter(UiElementNode node) {
        if (node != null) {
            UiElementNode parent = node.getUiParent();
            if (parent != null && parent instanceof UiViewElementNode) {
                return Pair.of((UiViewElementNode) parent, node.getUiSiblingIndex() + 1);
            }
        }

        return null;
    }

    /** Returns the pair [parent,index] of the first position inside the given parent */
    private static Pair<UiViewElementNode, Integer> getFirstPosition(UiElementNode parent) {
        if (parent != null && parent instanceof UiViewElementNode) {
            return Pair.of((UiViewElementNode) parent, 0);
        }

        return null;
    }

    /**
     * Returns the pair [parent,index] of the last position after the given node's
     * children
     */
    private static Pair<UiViewElementNode, Integer> getLastPosition(UiElementNode parent) {
        if (parent != null && parent instanceof UiViewElementNode) {
            return Pair.of((UiViewElementNode) parent, parent.getUiChildren().size());
        }

        return null;
    }

    /**
     * Truncates the given text such that it will fit into the given {@link StyledString}
     * up to a maximum length of {@link #LABEL_MAX_WIDTH}.
     *
     * @param text the text to truncate
     * @param string the existing string to be appended to
     * @return the truncated string
     */
    private static String truncate(String text, StyledString string) {
        int existingLength = string.length();

        if (text.length() + existingLength > LABEL_MAX_WIDTH) {
            int truncatedLength = LABEL_MAX_WIDTH - existingLength - 3;
            if (truncatedLength > 0) {
                return String.format("%1$s...", text.substring(0, truncatedLength));
            } else {
                return ""; //$NON-NLS-1$
            }
        }

        return text;
    }

    @Override
    public void setToolBar(IToolBarManager toolBarManager) {
        makeContributions(null, toolBarManager, null);
        toolBarManager.update(false);
    }

    /**
     * Sets up a custom tooltip when hovering over tree items. It currently displays the error
     * message for the lint warning associated with each node, if any (and only if the hover
     * is over the icon portion).
     */
    private void setupTooltip() {
        final Tree tree = getTreeViewer().getTree();

        // This is based on SWT Snippet 125
        final Listener listener = new Listener() {
            Shell mTip = null;
            Label mLabel = null;

            @Override
            public void handleEvent(Event event) {
                switch (event.type) {
                case SWT.Dispose:
                case SWT.KeyDown:
                case SWT.MouseExit:
                case SWT.MouseDown:
                case SWT.MouseMove:
                    if (mTip != null) {
                        mTip.dispose();
                        mTip = null;
                        mLabel = null;
                    }
                    break;
                case SWT.MouseHover:
                    if (mTip != null) {
                        mTip.dispose();
                        mTip = null;
                        mLabel = null;
                    }

                    String tooltip = null;

                    TreeItem item = tree.getItem(new Point(event.x, event.y));
                    if (item != null) {
                        Rectangle rect = item.getBounds(0);
                        if (event.x - rect.x > 16) { // 16: Standard width of our outline icons
                            return;
                        }

                        Object data = item.getData();
                        if (data != null && data instanceof CanvasViewInfo) {
                            LayoutEditorDelegate editor = mGraphicalEditorPart.getEditorDelegate();
                            CanvasViewInfo vi = (CanvasViewInfo) data;
                            IMarker marker = editor.getIssueForNode(vi.getUiViewNode());
                            if (marker != null) {
                                tooltip = marker.getAttribute(IMarker.MESSAGE, null);
                            }
                        }

                        if (tooltip != null) {
                            Shell shell = tree.getShell();
                            Display display = tree.getDisplay();

                            Color fg = display.getSystemColor(SWT.COLOR_INFO_FOREGROUND);
                            Color bg = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND);
                            mTip = new Shell(shell, SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL);
                            mTip.setBackground(bg);
                            FillLayout layout = new FillLayout();
                            layout.marginWidth = 1;
                            layout.marginHeight = 1;
                            mTip.setLayout(layout);
                            mLabel = new Label(mTip, SWT.WRAP);
                            mLabel.setForeground(fg);
                            mLabel.setBackground(bg);
                            mLabel.setText(tooltip);
                            mLabel.addListener(SWT.MouseExit, this);
                            mLabel.addListener(SWT.MouseDown, this);

                            Point pt = tree.toDisplay(rect.x, rect.y + rect.height);
                            Rectangle displayBounds = display.getBounds();
                            // -10: Don't extend -all- the way to the edge of the screen
                            // which would make it look like it has been cropped
                            int availableWidth = displayBounds.x + displayBounds.width - pt.x - 10;
                            if (availableWidth < 80) {
                                availableWidth = 80;
                            }
                            Point size = mTip.computeSize(SWT.DEFAULT, SWT.DEFAULT);
                            if (size.x > availableWidth) {
                                size = mTip.computeSize(availableWidth, SWT.DEFAULT);
                            }
                            mTip.setBounds(pt.x, pt.y, size.x, size.y);

                            mTip.setVisible(true);
                        }
                    }
                }
            }
        };

        tree.addListener(SWT.Dispose, listener);
        tree.addListener(SWT.KeyDown, listener);
        tree.addListener(SWT.MouseMove, listener);
        tree.addListener(SWT.MouseHover, listener);
    }
}