aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java1439
1 files changed, 1439 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java
new file mode 100644
index 000000000..8178c6871
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java
@@ -0,0 +1,1439 @@
+/*
+ * 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-1$
+
+ @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);
+ }
+}