diff options
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.java | 1439 |
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); + } +} |