/******************************************************************************* * Copyright 2005-2006, CHISEL Group, University of Victoria, Victoria, BC, * Canada. All rights reserved. This program and the accompanying materials are * made available under the terms of the Eclipse Public License v1.0 which * accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: The Chisel Group, University of Victoria ******************************************************************************/ package org.eclipse.zest.core.viewers.internal; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.TreeSet; import org.eclipse.draw2d.IFigure; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTError; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.widgets.Widget; import org.eclipse.zest.core.viewers.AbstractZoomableViewer; import org.eclipse.zest.core.viewers.IGraphContentProvider; import org.eclipse.zest.core.widgets.CGraphNode; import org.eclipse.zest.core.widgets.ConstraintAdapter; import org.eclipse.zest.core.widgets.Graph; import org.eclipse.zest.core.widgets.GraphConnection; import org.eclipse.zest.core.widgets.GraphContainer; import org.eclipse.zest.core.widgets.GraphItem; import org.eclipse.zest.core.widgets.GraphNode; import org.eclipse.zest.core.widgets.IContainer; import org.eclipse.zest.core.widgets.ZestStyles; import org.eclipse.zest.layouts.LayoutAlgorithm; /* * Abstraction of graph viewers to implement functionality used by all of them. * Not intended to be implemented by clients. Use one of the provided children * instead. * * @author Del Myers */ public abstract class AbstractStructuredGraphViewer extends AbstractZoomableViewer { /** * Contains top-level styles for the entire graph. Set in the constructor. * */ private int graphStyle; /** * Contains node-level styles for the graph. Set in setNodeStyle(). Defaults * are used in the constructor. */ private int nodeStyle; /** * Contains arc-level styles for the graph. Set in setConnectionStyle(). * Defaults are used in the constructor. */ private int connectionStyle; private HashMap nodesMap = new HashMap(); private HashMap connectionsMap = new HashMap(); /** * The constraint adatpers */ private List constraintAdapters = new ArrayList(); /** * A simple graph comparator that orders graph elements based on thier type * (connection or node), and their unique object identification. */ private class SimpleGraphComparator implements Comparator { TreeSet storedStrings; /** * */ public SimpleGraphComparator() { this.storedStrings = new TreeSet(); } /* * (non-Javadoc) * * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object) */ public int compare(Object arg0, Object arg1) { if (arg0 instanceof GraphNode && arg1 instanceof GraphConnection) { return 1; } else if (arg0 instanceof GraphConnection && arg1 instanceof GraphNode) { return -1; } if (arg0.equals(arg1)) { return 0; } return getObjectString(arg0).compareTo(getObjectString(arg1)); } private String getObjectString(Object o) { String s = o.getClass().getName() + "@" + Integer.toHexString(o.hashCode()); while (storedStrings.contains(s)) { s = s + 'X'; } return s; } } protected AbstractStructuredGraphViewer(int graphStyle) { this.graphStyle = graphStyle; this.connectionStyle = SWT.NONE; this.nodeStyle = SWT.NONE; } /** * Sets the default style for nodes in this graph. Note: if an input is set * on the viewer, a ZestException will be thrown. * * @param nodeStyle * the style for the nodes. * @see #ZestStyles */ public void setNodeStyle(int nodeStyle) { if (getInput() != null) { throw new SWTError(SWT.ERROR_UNSPECIFIED); } this.nodeStyle = nodeStyle; } /** * Sets the default style for connections in this graph. Note: if an input * is set on the viewer, a ZestException will be thrown. * * @param connectionStyle * the style for the connections. * @see #ZestStyles */ public void setConnectionStyle(int connectionStyle) { if (getInput() != null) { throw new SWTError(SWT.ERROR_UNSPECIFIED); } if (!ZestStyles.validateConnectionStyle(connectionStyle)) { throw new SWTError(SWT.ERROR_INVALID_ARGUMENT); } this.connectionStyle = connectionStyle; } /** * Returns the style set for the graph * * @return The style set of the graph */ public int getGraphStyle() { return graphStyle; } /** * Returns the style set for the nodes. * * @return the style set for the nodes. */ public int getNodeStyle() { return nodeStyle; } public Graph getGraphControl() { return (Graph) getControl(); } /** * @return the connection style. */ public int getConnectionStyle() { return connectionStyle; } /** * Adds a new constraint adapter to the list of constraints * * @param constraintAdapter */ public void addConstraintAdapter(ConstraintAdapter constraintAdapter) { this.constraintAdapters.add(constraintAdapter); } /** * Gets all the constraint adapters currently on the viewer * * @return */ public List getConstraintAdapters() { return this.constraintAdapters; } /** * Sets the layout algorithm for this viewer. Subclasses may place * restrictions on the algorithms that it accepts. * * @param algorithm * the layout algorithm * @param run * true if the layout algorithm should be run immediately. This * is a hint. */ public abstract void setLayoutAlgorithm(LayoutAlgorithm algorithm, boolean run); /** * Gets the current layout algorithm. * * @return the current layout algorithm. */ protected abstract LayoutAlgorithm getLayoutAlgorithm(); /** * Equivalent to setLayoutAlgorithm(algorithm, false). * * @param algorithm */ public void setLayoutAlgorithm(LayoutAlgorithm algorithm) { setLayoutAlgorithm(algorithm, false); } public Object[] getNodeElements() { return this.nodesMap.keySet().toArray(); } public Object[] getConnectionElements() { return this.connectionsMap.keySet().toArray(); } HashMap getNodesMap() { return this.nodesMap; } GraphNode addGraphModelContainer(Object element) { GraphNode node = this.getGraphModelNode(element); if (node == null) { node = new GraphContainer((Graph) getControl(), SWT.NONE); this.nodesMap.put(element, node); node.setData(element); } return node; } GraphNode addGraphModelNode(IContainer container, Object element) { GraphNode node = this.getGraphModelNode(element); if (node == null) { node = new GraphNode(container, SWT.NONE); this.nodesMap.put(element, node); node.setData(element); } return node; } GraphNode addGraphModelNode(Object element, IFigure figure) { GraphNode node = this.getGraphModelNode(element); if (node == null) { if (figure != null) { node = new CGraphNode((Graph) getControl(), SWT.NONE, figure); this.nodesMap.put(element, node); node.setData(element); } else { node = new GraphNode((Graph) getControl(), SWT.NONE); this.nodesMap.put(element, node); node.setData(element); } } return node; } GraphConnection addGraphModelConnection(Object element, GraphNode source, GraphNode target) { GraphConnection connection = this.getGraphModelConnection(element); if (connection == null) { connection = new GraphConnection((Graph) getControl(), SWT.NONE, source, target); this.connectionsMap.put(element, connection); connection.setData(element); } return connection; } GraphConnection getGraphModelConnection(Object obj) { return (GraphConnection) this.connectionsMap.get(obj); } GraphNode getGraphModelNode(Object obj) { return (GraphNode) this.nodesMap.get(obj); } void removeGraphModelConnection(Object obj) { GraphConnection connection = (GraphConnection) connectionsMap.get(obj); if (connection != null) { connectionsMap.remove(obj); if (!connection.isDisposed()) { connection.dispose(); } } } void removeGraphModelNode(Object obj) { GraphNode node = (GraphNode) nodesMap.get(obj); if (node != null) { nodesMap.remove(obj); if (!node.isDisposed()) { node.dispose(); } } } protected void handleDispose(DisposeEvent event) { if (getControl() != null && !getControl().isDisposed()) { getControl().dispose(); } super.handleDispose(event); } /* * (non-Javadoc) * * @see org.eclipse.jface.viewers.StructuredViewer#internalRefresh(java.lang.Object) */ protected void internalRefresh(Object element) { if (getInput() == null) { return; } if (element == getInput()) { getFactory().refreshGraph(getGraphControl()); } else { getFactory().refresh(getGraphControl(), element); } // After all the items are loaded, we call update to ensure drawing. // This way the damaged area does not get too big if we start // adding and removing more nodes getGraphControl().getLightweightSystem().getUpdateManager().performUpdate(); } protected void doUpdateItem(Widget item, Object element, boolean fullMap) { if (item == getGraphControl()) { getFactory().update(getNodesArray(getGraphControl())); getFactory().update(getConnectionsArray(getGraphControl())); } else if (item instanceof GraphItem) { getFactory().update((GraphItem) item); } } /* * (non-Javadoc) * * @see org.eclipse.jface.viewers.StructuredViewer#doFindInputItem(java.lang.Object) */ protected Widget doFindInputItem(Object element) { if (element == getInput() && element instanceof Widget) { return (Widget) element; } return null; } /* * (non-Javadoc) * * @see org.eclipse.jface.viewers.StructuredViewer#doFindItem(java.lang.Object) */ protected Widget doFindItem(Object element) { Widget node = (Widget) nodesMap.get(element); Widget connection = (Widget) connectionsMap.get(element); return (node != null) ? node : connection; } /* * (non-Javadoc) * * @see org.eclipse.jface.viewers.StructuredViewer#getSelectionFromWidget() */ protected List getSelectionFromWidget() { List internalSelection = getWidgetSelection(); LinkedList externalSelection = new LinkedList(); for (Iterator i = internalSelection.iterator(); i.hasNext();) { // @tag zest.todo : should there be a method on IGraphItem to get // the external data? GraphItem item = (GraphItem) i.next(); if (item instanceof GraphNode) { externalSelection.add(((GraphNode) item).getData()); } else if (item instanceof GraphConnection) { externalSelection.add(((GraphConnection) item).getExternalConnection()); } else if (item instanceof Widget) { externalSelection.add(((Widget) item).getData()); } } return externalSelection; } protected GraphItem[] /* GraphItem */findItems(List l) { if (l == null) { return new GraphItem[0]; } ArrayList list = new ArrayList(); Iterator iterator = l.iterator(); while (iterator.hasNext()) { GraphItem w = (GraphItem) findItem(iterator.next()); list.add(w); } return (GraphItem[]) list.toArray(new GraphItem[list.size()]); } /* * (non-Javadoc) * * @see org.eclipse.jface.viewers.StructuredViewer#setSelectionToWidget(java.util.List, * boolean) */ protected void setSelectionToWidget(List l, boolean reveal) { Graph control = (Graph) getControl(); List selection = new LinkedList(); for (Iterator i = l.iterator(); i.hasNext();) { Object obj = i.next(); GraphNode node = (GraphNode) nodesMap.get(obj); GraphConnection conn = (GraphConnection) connectionsMap.get(obj); if (node != null) { selection.add(node); } if (conn != null) { selection.add(conn); } } control.setSelection((GraphNode[]) selection.toArray(new GraphNode[selection.size()])); } /** * Gets the internal model elements that are selected. * * @return */ protected List getWidgetSelection() { Graph control = (Graph) getControl(); return control.getSelection(); } /* * (non-Javadoc) * * @see org.eclipse.jface.viewers.Viewer#inputChanged(java.lang.Object, * java.lang.Object) */ protected void inputChanged(Object input, Object oldInput) { IStylingGraphModelFactory factory = getFactory(); factory.setConnectionStyle(getConnectionStyle()); factory.setNodeStyle(getNodeStyle()); // Save the old map so we can set the size and position of any nodes // that are the same Map oldNodesMap = nodesMap; Graph graph = (Graph) getControl(); graph.setSelection(new GraphNode[0]); Iterator iterator = nodesMap.values().iterator(); while (iterator.hasNext()) { GraphNode node = (GraphNode) iterator.next(); if (!node.isDisposed()) { node.dispose(); } } iterator = connectionsMap.values().iterator(); while (iterator.hasNext()) { GraphConnection connection = (GraphConnection) iterator.next(); if (!connection.isDisposed()) { connection.dispose(); } } nodesMap = new HashMap(); connectionsMap = new HashMap(); graph = factory.createGraphModel(graph); ((Graph) getControl()).setNodeStyle(getNodeStyle()); ((Graph) getControl()).setConnectionStyle(getConnectionStyle()); // check if any of the pre-existing nodes are still present // in this case we want them to keep the same location & size for (Iterator iter = oldNodesMap.keySet().iterator(); iter.hasNext();) { Object data = iter.next(); GraphNode newNode = (GraphNode) nodesMap.get(data); if (newNode != null) { GraphNode oldNode = (GraphNode) oldNodesMap.get(data); newNode.setLocation(oldNode.getLocation().x, oldNode.getLocation().y); if (oldNode.isSizeFixed()) { newNode.setSize(oldNode.getSize().width, oldNode.getSize().height); } } } applyLayout(); } /** * Returns the factory used to create the model. This must not be called * before the content provider is set. * * @return */ protected abstract IStylingGraphModelFactory getFactory(); protected void filterVisuals() { if (getGraphControl() == null) { return; } Object[] filtered = getFilteredChildren(getInput()); SimpleGraphComparator comparator = new SimpleGraphComparator(); TreeSet filteredElements = new TreeSet(comparator); TreeSet unfilteredElements = new TreeSet(comparator); List connections = getGraphControl().getConnections(); List nodes = getGraphControl().getNodes(); if (filtered.length == 0) { // set everything to invisible. // @tag zest.bug.156528-Filters.check : should we only filter out // the nodes? for (Iterator i = connections.iterator(); i.hasNext();) { GraphConnection c = (GraphConnection) i.next(); c.setVisible(false); } for (Iterator i = nodes.iterator(); i.hasNext();) { GraphNode n = (GraphNode) i.next(); n.setVisible(false); } return; } for (Iterator i = connections.iterator(); i.hasNext();) { GraphConnection c = (GraphConnection) i.next(); if (c.getExternalConnection() != null) { unfilteredElements.add(c); } } for (Iterator i = nodes.iterator(); i.hasNext();) { GraphNode n = (GraphNode) i.next(); if (n.getData() != null) { unfilteredElements.add(n); } } for (int i = 0; i < filtered.length; i++) { Object modelElement = connectionsMap.get(filtered[i]); if (modelElement == null) { modelElement = nodesMap.get(filtered[i]); } if (modelElement != null) { filteredElements.add(modelElement); } } unfilteredElements.removeAll(filteredElements); // set all the elements that did not pass the filters to invisible, and // all the elements that passed to visible. while (unfilteredElements.size() > 0) { GraphItem i = (GraphItem) unfilteredElements.first(); i.setVisible(false); unfilteredElements.remove(i); } while (filteredElements.size() > 0) { GraphItem i = (GraphItem) filteredElements.first(); i.setVisible(true); filteredElements.remove(i); } } /* * (non-Javadoc) * * @see org.eclipse.jface.viewers.StructuredViewer#getRawChildren(java.lang.Object) */ protected Object[] getRawChildren(Object parent) { if (parent == getInput()) { // get the children from the model. LinkedList children = new LinkedList(); if (getGraphControl() != null) { List connections = getGraphControl().getConnections(); List nodes = getGraphControl().getNodes(); for (Iterator i = connections.iterator(); i.hasNext();) { GraphConnection c = (GraphConnection) i.next(); if (c.getExternalConnection() != null) { children.add(c.getExternalConnection()); } } for (Iterator i = nodes.iterator(); i.hasNext();) { GraphNode n = (GraphNode) i.next(); if (n.getData() != null) { children.add(n.getData()); } } return children.toArray(); } } return super.getRawChildren(parent); } /** * */ public void reveal(Object element) { Widget[] items = this.findItems(element); for (int i = 0; i < items.length; i++) { Widget item = items[i]; if (item instanceof GraphNode) { GraphNode graphModelNode = (GraphNode) item; graphModelNode.highlight(); } else if (item instanceof GraphConnection) { GraphConnection graphModelConnection = (GraphConnection) item; graphModelConnection.highlight(); } } } public void unReveal(Object element) { Widget[] items = this.findItems(element); for (int i = 0; i < items.length; i++) { Widget item = items[i]; if (item instanceof GraphNode) { GraphNode graphModelNode = (GraphNode) item; graphModelNode.unhighlight(); } else if (item instanceof GraphConnection) { GraphConnection graphModelConnection = (GraphConnection) item; graphModelConnection.unhighlight(); } } } /** * Applies the viewers layouts. * */ public abstract void applyLayout(); /** * Removes the given connection object from the layout algorithm and the * model. * * @param connection */ public void removeRelationship(Object connection) { GraphConnection relationship = (GraphConnection) connectionsMap.get(connection); if (relationship != null) { // remove the relationship from the layout algorithm if (getLayoutAlgorithm() != null) { getLayoutAlgorithm().removeRelationship(relationship.getLayoutRelationship()); } // remove the relationship from the model relationship.dispose(); } } /** * Creates a new node and adds it to the graph. If it already exists nothing * happens. * * @param newNode */ public void addNode(Object element) { if (nodesMap.get(element) == null) { // create the new node getFactory().createNode(getGraphControl(), element); } } /** * Removes the given element from the layout algorithm and the model. * * @param element * The node element to remove. */ public void removeNode(Object element) { GraphNode node = (GraphNode) nodesMap.get(element); if (node != null) { // remove the node from the layout algorithm and all the connections if (getLayoutAlgorithm() != null) { getLayoutAlgorithm().removeEntity(node.getLayoutEntity()); getLayoutAlgorithm().removeRelationships(node.getSourceConnections()); getLayoutAlgorithm().removeRelationships(node.getTargetConnections()); } // remove the node and it's connections from the model node.dispose(); } } /** * Creates a new relationship between the source node and the destination * node. If either node doesn't exist then it will be created. * * @param connection * The connection data object. * @param srcNode * The source node data object. * @param destNode * The destination node data object. */ public void addRelationship(Object connection, Object srcNode, Object destNode) { // create the new relationship IStylingGraphModelFactory modelFactory = getFactory(); modelFactory.createConnection(getGraphControl(), connection, srcNode, destNode); } /** * Adds a new relationship given the connection. It will use the content * provider to determine the source and destination nodes. * * @param connection * The connection data object. */ public void addRelationship(Object connection) { IStylingGraphModelFactory modelFactory = getFactory(); if (connectionsMap.get(connection) == null) { if (modelFactory.getContentProvider() instanceof IGraphContentProvider) { IGraphContentProvider content = ((IGraphContentProvider) modelFactory.getContentProvider()); Object source = content.getSource(connection); Object dest = content.getDestination(connection); // create the new relationship modelFactory.createConnection(getGraphControl(), connection, source, dest); } else { throw new UnsupportedOperationException(); } } } /** * Converts the list of GraphModelConnection objects into an array and * returns it. * * @return GraphModelConnection[] */ protected GraphConnection[] getConnectionsArray(Graph graph) { GraphConnection[] connsArray = new GraphConnection[graph.getConnections().size()]; connsArray = (GraphConnection[]) graph.getConnections().toArray(connsArray); return connsArray; } /** * Converts the list of GraphModelNode objects into an array an returns it. * * @return GraphModelNode[] */ protected GraphNode[] getNodesArray(Graph graph) { GraphNode[] nodesArray = new GraphNode[graph.getNodes().size()]; nodesArray = (GraphNode[]) graph.getNodes().toArray(nodesArray); return nodesArray; } }