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