| /* |
| * 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); |
| } |
| } |