| /* |
| * 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.adt.internal.editors.layout; |
| |
| import com.android.ide.eclipse.adt.internal.editors.IconFactory; |
| import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiDocumentTreeEditPart; |
| import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiElementTreeEditPart; |
| import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiElementTreeEditPartFactory; |
| import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiLayoutTreeEditPart; |
| import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiViewTreeEditPart; |
| import com.android.ide.eclipse.adt.internal.editors.ui.tree.CopyCutAction; |
| import com.android.ide.eclipse.adt.internal.editors.ui.tree.PasteAction; |
| import com.android.ide.eclipse.adt.internal.editors.ui.tree.UiActions; |
| import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; |
| import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; |
| import com.android.ide.eclipse.adt.internal.ui.EclipseUiHelper; |
| |
| 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. |
| } |
| |
| } |
| } |