/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 * * 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.editors.layout; import com.android.ide.eclipse.adt.ui.EclipseUiHelper; import com.android.ide.eclipse.editors.IconFactory; import com.android.ide.eclipse.editors.layout.parts.UiDocumentTreeEditPart; import com.android.ide.eclipse.editors.layout.parts.UiElementTreeEditPart; import com.android.ide.eclipse.editors.layout.parts.UiElementTreeEditPartFactory; import com.android.ide.eclipse.editors.layout.parts.UiLayoutTreeEditPart; import com.android.ide.eclipse.editors.layout.parts.UiViewTreeEditPart; import com.android.ide.eclipse.editors.ui.tree.CopyCutAction; import com.android.ide.eclipse.editors.ui.tree.PasteAction; import com.android.ide.eclipse.editors.ui.tree.UiActions; import com.android.ide.eclipse.editors.uimodel.UiDocumentNode; import com.android.ide.eclipse.editors.uimodel.UiElementNode; import org.eclipse.gef.EditPartViewer; import org.eclipse.gef.ui.parts.ContentOutlinePage; import org.eclipse.jface.action.Action; 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.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TreePath; import org.eclipse.jface.viewers.TreeSelection; import org.eclipse.swt.SWT; 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.Menu; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; import org.eclipse.ui.IActionBars; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; /** * Implementation of the {@link ContentOutlinePage} to display {@link UiElementNode}. */ class UiContentOutlinePage extends ContentOutlinePage { private AbstractGraphicalLayoutEditor mEditor; private Action mAddAction; private Action mDeleteAction; private Action mUpAction; private Action mDownAction; private UiOutlineActions mUiActions = new UiOutlineActions(); public UiContentOutlinePage(AbstractGraphicalLayoutEditor editor, final EditPartViewer viewer) { super(viewer); mEditor = editor; IconFactory factory = IconFactory.getInstance(); mAddAction = new Action("Add...") { @Override public void run() { List<UiElementNode> nodes = getModelSelections(); UiElementNode node = nodes != null && nodes.size() > 0 ? nodes.get(0) : null; mUiActions.doAdd(node, viewer.getControl().getShell()); } }; mAddAction.setToolTipText("Adds a new element."); mAddAction.setImageDescriptor(factory.getImageDescriptor("add")); //$NON-NLS-1$ mDeleteAction = new Action("Remove...") { @Override public void run() { List<UiElementNode> nodes = getModelSelections(); mUiActions.doRemove(nodes, viewer.getControl().getShell()); } }; mDeleteAction.setToolTipText("Removes an existing selected element."); mDeleteAction.setImageDescriptor(factory.getImageDescriptor("delete")); //$NON-NLS-1$ mUpAction = new Action("Up") { @Override public void run() { List<UiElementNode> nodes = getModelSelections(); mUiActions.doUp(nodes); } }; mUpAction.setToolTipText("Moves the selected element up"); mUpAction.setImageDescriptor(factory.getImageDescriptor("up")); //$NON-NLS-1$ mDownAction = new Action("Down") { @Override public void run() { List<UiElementNode> nodes = getModelSelections(); mUiActions.doDown(nodes); } }; mDownAction.setToolTipText("Moves the selected element down"); mDownAction.setImageDescriptor(factory.getImageDescriptor("down")); //$NON-NLS-1$ // all actions disabled by default. mAddAction.setEnabled(false); mDeleteAction.setEnabled(false); mUpAction.setEnabled(false); mDownAction.setEnabled(false); addSelectionChangedListener(new ISelectionChangedListener() { public void selectionChanged(SelectionChangedEvent event) { ISelection selection = event.getSelection(); // the selection is never empty. The least it'll contain is the // UiDocumentTreeEditPart object. if (selection instanceof StructuredSelection) { StructuredSelection structSel = (StructuredSelection)selection; if (structSel.size() == 1 && structSel.getFirstElement() instanceof UiDocumentTreeEditPart) { mDeleteAction.setEnabled(false); mUpAction.setEnabled(false); mDownAction.setEnabled(false); } else { mDeleteAction.setEnabled(true); mUpAction.setEnabled(true); mDownAction.setEnabled(true); } // the "add" button is always enabled, in order to be able to set the // initial root node mAddAction.setEnabled(true); } } }); } /* (non-Javadoc) * @see org.eclipse.ui.part.IPage#createControl(org.eclipse.swt.widgets.Composite) */ @Override public void createControl(Composite parent) { // create outline viewer page getViewer().createControl(parent); // configure outline viewer getViewer().setEditPartFactory(new UiElementTreeEditPartFactory()); setupOutline(); setupContextMenu(); setupTooltip(); setupDoubleClick(); } /* * (non-Javadoc) * @see org.eclipse.ui.part.Page#setActionBars(org.eclipse.ui.IActionBars) * * Called automatically after createControl */ @Override public void setActionBars(IActionBars actionBars) { IToolBarManager toolBarManager = actionBars.getToolBarManager(); toolBarManager.add(mAddAction); toolBarManager.add(mDeleteAction); toolBarManager.add(new Separator()); toolBarManager.add(mUpAction); toolBarManager.add(mDownAction); IMenuManager menuManager = actionBars.getMenuManager(); menuManager.add(mAddAction); menuManager.add(mDeleteAction); menuManager.add(new Separator()); menuManager.add(mUpAction); menuManager.add(mDownAction); } /* (non-Javadoc) * @see org.eclipse.ui.part.IPage#dispose() */ @Override public void dispose() { breakConnectionWithEditor(); // dispose super.dispose(); } /* (non-Javadoc) * @see org.eclipse.ui.part.IPage#getControl() */ @Override public Control getControl() { return getViewer().getControl(); } void setNewEditor(GraphicalLayoutEditor editor) { mEditor = editor; setupOutline(); } void breakConnectionWithEditor() { // unhook outline viewer mEditor.getSelectionSynchronizer().removeViewer(getViewer()); } private void setupOutline() { getViewer().setEditDomain(mEditor.getEditDomain()); // hook outline viewer mEditor.getSelectionSynchronizer().addViewer(getViewer()); // initialize outline viewer with model getViewer().setContents(mEditor.getModel()); } private void setupContextMenu() { MenuManager menuManager = new MenuManager(); menuManager.setRemoveAllWhenShown(true); menuManager.addMenuListener(new IMenuListener() { /** * The menu is about to be shown. The menu manager has already been * requested to remove any existing menu item. This method gets the * tree selection and if it is of the appropriate type it re-creates * the necessary actions. */ public void menuAboutToShow(IMenuManager manager) { List<UiElementNode> selected = getModelSelections(); if (selected != null) { doCreateMenuAction(manager, selected); return; } doCreateMenuAction(manager, null /* ui_node */); } }); Control control = getControl(); Menu contextMenu = menuManager.createContextMenu(control); control.setMenu(contextMenu); } /** * Adds the menu actions to the context menu when the given UI node is selected in * the tree view. * * @param manager The context menu manager * @param selected The UI node selected in the tree. Can be null, in which case the root * is to be modified. */ private void doCreateMenuAction(IMenuManager manager, List<UiElementNode> selected) { if (selected != null) { boolean hasXml = false; for (UiElementNode uiNode : selected) { if (uiNode.getXmlNode() != null) { hasXml = true; break; } } if (hasXml) { manager.add(new CopyCutAction(mEditor.getLayoutEditor(), mEditor.getClipboard(), null, selected, true /* cut */)); manager.add(new CopyCutAction(mEditor.getLayoutEditor(), mEditor.getClipboard(), null, selected, false /* cut */)); // Can't paste with more than one element selected (the selection is the target) if (selected.size() <= 1) { // Paste is not valid if it would add a second element on a terminal element // which parent is a document -- an XML document can only have one child. This // means paste is valid if the current UI node can have children or if the parent // is not a document. UiElementNode ui_root = selected.get(0).getUiRoot(); if (ui_root.getDescriptor().hasChildren() || !(ui_root.getUiParent() instanceof UiDocumentNode)) { manager.add(new PasteAction(mEditor.getLayoutEditor(), mEditor.getClipboard(), selected.get(0))); } } manager.add(new Separator()); } } // Append "add" and "remove" actions. They do the same thing as the add/remove // buttons on the side. // // "Add" makes sense only if there's 0 or 1 item selected since the // one selected item becomes the target. if (selected == null || selected.size() <= 1) { manager.add(mAddAction); } if (selected != null) { manager.add(mDeleteAction); manager.add(new Separator()); manager.add(mUpAction); manager.add(mDownAction); } if (selected != null && selected.size() == 1) { manager.add(new Separator()); Action propertiesAction = new Action("Properties") { @Override public void run() { EclipseUiHelper.showView(EclipseUiHelper.PROPERTY_SHEET_VIEW_ID, true /* activate */); } }; propertiesAction.setToolTipText("Displays properties of the selected element."); manager.add(propertiesAction); } } /** * Updates the outline view with the model of the {@link GraphicalLayoutEditor}. * <p/> * This attemps to preserve the selection, if any. */ public void reloadModel() { // Attemps to preserve the UiNode selection, if any List<UiElementNode> uiNodes = null; try { // get current selection using the model rather than the edit part as // reloading the content may change the actual edit part. uiNodes = getModelSelections(); // perform the update getViewer().setContents(mEditor.getModel()); } finally { // restore selection if (uiNodes != null) { setModelSelection(uiNodes.get(0)); } } } /** * Returns the currently selected element, if any, in the viewer. * This returns the viewer's elements (i.e. an {@link UiElementTreeEditPart}) * and not the underlying model node. * <p/> * When there is no actual selection, this might still return the root node, * which is of type {@link UiDocumentTreeEditPart}. */ @SuppressWarnings("unchecked") private List<UiElementTreeEditPart> getViewerSelections() { ISelection selection = getSelection(); if (selection instanceof StructuredSelection) { StructuredSelection structuredSelection = (StructuredSelection)selection; if (structuredSelection.size() > 0) { ArrayList<UiElementTreeEditPart> selected = new ArrayList<UiElementTreeEditPart>(); for (Iterator it = structuredSelection.iterator(); it.hasNext(); ) { Object selectedObj = it.next(); if (selectedObj instanceof UiElementTreeEditPart) { selected.add((UiElementTreeEditPart) selectedObj); } } return selected.size() > 0 ? selected : null; } } return null; } /** * Returns the currently selected model element, which is either an * {@link UiViewTreeEditPart} or an {@link UiLayoutTreeEditPart}. * <p/> * Returns null if there is no selection or if the implicit root is "selected" * (which actually represents the lack of a real element selection.) */ private List<UiElementNode> getModelSelections() { List<UiElementTreeEditPart> parts = getViewerSelections(); if (parts != null) { ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>(); for (UiElementTreeEditPart part : parts) { if (part instanceof UiViewTreeEditPart || part instanceof UiLayoutTreeEditPart) { selected.add((UiElementNode) part.getModel()); } } return selected.size() > 0 ? selected : null; } return null; } /** * Selects the corresponding edit part in the tree viewer. */ private void setViewerSelection(UiElementTreeEditPart selectedPart) { if (selectedPart != null && !(selectedPart instanceof UiDocumentTreeEditPart)) { LinkedList<UiElementTreeEditPart> segments = new LinkedList<UiElementTreeEditPart>(); for (UiElementTreeEditPart part = selectedPart; !(part instanceof UiDocumentTreeEditPart); part = (UiElementTreeEditPart) part.getParent()) { segments.add(0, part); } setSelection(new TreeSelection(new TreePath(segments.toArray()))); } } /** * Selects the corresponding model element in the tree viewer. */ private void setModelSelection(UiElementNode uiNodeToSelect) { if (uiNodeToSelect != null) { // find an edit part that has the requested model element UiElementTreeEditPart part = findPartForModel( (UiElementTreeEditPart) getViewer().getContents(), uiNodeToSelect); // if we found a part, select it and reveal it if (part != null) { setViewerSelection(part); getViewer().reveal(part); } } } /** * Utility method that tries to find an edit part that matches a given model UI node. * * @param rootPart The root of the viewer edit parts * @param uiNode The UI node model to find * @return The part that matches the model or null if it's not in the sub tree. */ private UiElementTreeEditPart findPartForModel(UiElementTreeEditPart rootPart, UiElementNode uiNode) { if (rootPart.getModel() == uiNode) { return rootPart; } for (Object part : rootPart.getChildren()) { if (part instanceof UiElementTreeEditPart) { UiElementTreeEditPart found = findPartForModel( (UiElementTreeEditPart) part, uiNode); if (found != null) { return found; } } } return null; } /** * Sets up a custom tooltip when hovering over tree items. * <p/> * The tooltip will display the element's javadoc, if any, or the item's getText otherwise. */ private void setupTooltip() { final Tree tree = (Tree) getControl(); /* * Reference: * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet125.java?view=markup */ final Listener listener = new Listener() { Shell tip = null; Label label = null; public void handleEvent(Event event) { switch(event.type) { case SWT.Dispose: case SWT.KeyDown: case SWT.MouseExit: case SWT.MouseDown: case SWT.MouseMove: if (tip != null) { tip.dispose(); tip = null; label = null; } break; case SWT.MouseHover: if (tip != null) { tip.dispose(); tip = null; label = null; } String tooltip = null; TreeItem item = tree.getItem(new Point(event.x, event.y)); if (item != null) { Object data = item.getData(); if (data instanceof UiElementTreeEditPart) { Object model = ((UiElementTreeEditPart) data).getModel(); if (model instanceof UiElementNode) { tooltip = ((UiElementNode) model).getDescriptor().getTooltip(); } } if (tooltip == null) { tooltip = item.getText(); } else { tooltip = item.getText() + ":\r" + tooltip; } } if (tooltip != null) { Shell shell = tree.getShell(); Display display = tree.getDisplay(); tip = new Shell(shell, SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL); tip.setBackground(display .getSystemColor(SWT.COLOR_INFO_BACKGROUND)); FillLayout layout = new FillLayout(); layout.marginWidth = 2; tip.setLayout(layout); label = new Label(tip, SWT.NONE); label.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); label.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); label.setData("_TABLEITEM", item); label.setText(tooltip); label.addListener(SWT.MouseExit, this); label.addListener(SWT.MouseDown, this); Point size = tip.computeSize(SWT.DEFAULT, SWT.DEFAULT); Rectangle rect = item.getBounds(0); Point pt = tree.toDisplay(rect.x, rect.y); tip.setBounds(pt.x, pt.y, size.x, size.y); tip.setVisible(true); } } } }; tree.addListener(SWT.Dispose, listener); tree.addListener(SWT.KeyDown, listener); tree.addListener(SWT.MouseMove, listener); tree.addListener(SWT.MouseHover, listener); } /** * Sets up double-click action on the tree. * <p/> * By default, double-click (a.k.a. "default selection") on a valid list item will * show the property view. */ private void setupDoubleClick() { final Tree tree = (Tree) getControl(); tree.addListener(SWT.DefaultSelection, new Listener() { public void handleEvent(Event event) { EclipseUiHelper.showView(EclipseUiHelper.PROPERTY_SHEET_VIEW_ID, true /* activate */); } }); } // --------------- private class UiOutlineActions extends UiActions { @Override protected UiDocumentNode getRootNode() { return mEditor.getModel(); // this is LayoutEditor.getUiRootNode() } // Select the new item @Override protected void selectUiNode(UiElementNode uiNodeToSelect) { setModelSelection(uiNodeToSelect); } @Override public void commitPendingXmlChanges() { // Pass. There is nothing to commit before the XML is changed here. } } }