/******************************************************************************* * Copyright 2005, 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.widgets; import org.eclipse.draw2d.ChopboxAnchor; import org.eclipse.draw2d.ColorConstants; import org.eclipse.draw2d.Connection; import org.eclipse.draw2d.Graphics; import org.eclipse.draw2d.IFigure; import org.eclipse.draw2d.Label; import org.eclipse.draw2d.Locator; import org.eclipse.draw2d.MidpointLocator; import org.eclipse.draw2d.PolygonDecoration; import org.eclipse.draw2d.PolylineConnection; import org.eclipse.draw2d.Shape; import org.eclipse.draw2d.geometry.Point; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.widgets.Display; import org.eclipse.zest.core.widgets.internal.LoopAnchor; import org.eclipse.zest.core.widgets.internal.PolylineArcConnection; import org.eclipse.zest.core.widgets.internal.RoundedChopboxAnchor; import org.eclipse.zest.core.widgets.internal.ZestRootLayer; import org.eclipse.zest.layouts.LayoutBendPoint; import org.eclipse.zest.layouts.LayoutEntity; import org.eclipse.zest.layouts.LayoutRelationship; import org.eclipse.zest.layouts.constraints.LayoutConstraint; /* * This is the graph connection model which stores the source and destination * nodes and the properties of this connection (color, line width etc). * * @author Chris Callendar * * @author Ian Bull */ public class GraphConnection extends GraphItem { private Font font; private GraphNode sourceNode; private GraphNode destinationNode; private double weight; private Color color; private Color highlightColor; private Color foreground; private int lineWidth; private int lineStyle; private final Graph graphModel; private int connectionStyle; private int curveDepth; private boolean isDisposed = false; private Label connectionLabel = null; private Connection connectionFigure = null; private Connection sourceContainerConnectionFigure = null; private Connection targetContainerConnectionFigure = null; /** * The state of visibility set by the user. */ private boolean visible; private IFigure tooltip; private boolean highlighted; private GraphLayoutConnection layoutConnection = null; private boolean hasCustomTooltip; public GraphConnection(Graph graphModel, int style, GraphNode source, GraphNode destination) { super(graphModel, style); this.connectionStyle |= graphModel.getConnectionStyle(); this.connectionStyle |= style; this.sourceNode = source; this.destinationNode = destination; this.visible = true; this.color = ColorConstants.lightGray; this.foreground = ColorConstants.lightGray; this.highlightColor = graphModel.DARK_BLUE; this.lineWidth = 1; this.lineStyle = Graphics.LINE_SOLID; setWeight(weight); this.graphModel = graphModel; this.curveDepth = 0; this.layoutConnection = new GraphLayoutConnection(); this.font = Display.getDefault().getSystemFont(); registerConnection(source, destination); } private void registerConnection(GraphNode source, GraphNode destination) { if (source.getSourceConnections().contains(this)) { source.removeSourceConnection(this); } if (destination.getTargetConnections().contains(this)) { destination.removeTargetConnection(this); } (source).addSourceConnection(this); (destination).addTargetConnection(this); if (source.getParent().getItemType() == GraphItem.CONTAINER && destination.getParent().getItemType() == GraphItem.CONTAINER && (source.getParent() == destination.getParent())) { // 196189: Edges should not draw on the edge layer if both the src and dest are in the same container // https://bugs.eclipse.org/bugs/show_bug.cgi?id=196189 graphModel.addConnection(this, ZestRootLayer.EDGES_ON_TOP); } else { graphModel.addConnection(this, true); } if ((source.getParent()).getItemType() == GraphItem.CONTAINER) { // If the container of the source is a container, we need to draw another // arc on that arc layer sourceContainerConnectionFigure = doCreateFigure(); ((GraphContainer) source.getParent()).addConnectionFigure((PolylineConnection) sourceContainerConnectionFigure); this.setVisible(false); } if ((destination.getParent()).getItemType() == GraphItem.CONTAINER) { //&& src_destSameContainer == false) { // If the container of the source is a container, we need to draw another // arc on that arc layer targetContainerConnectionFigure = doCreateFigure(); ((GraphContainer) destination.getParent()).addConnectionFigure((PolylineConnection) targetContainerConnectionFigure); this.setVisible(false); } graphModel.getGraph().registerItem(this); } void removeFigure() { if (connectionFigure.getParent() != null) { if (connectionFigure.getParent() instanceof ZestRootLayer) { ((ZestRootLayer) connectionFigure.getParent()).removeConnection(connectionFigure); } else { connectionFigure.getParent().remove(connectionFigure); } } connectionFigure = null; if (sourceContainerConnectionFigure != null) { sourceContainerConnectionFigure.getParent().remove(sourceContainerConnectionFigure); sourceContainerConnectionFigure = null; } if (targetContainerConnectionFigure != null) { targetContainerConnectionFigure.getParent().remove(targetContainerConnectionFigure); targetContainerConnectionFigure = null; } } public void dispose() { super.dispose(); this.isDisposed = true; (getSource()).removeSourceConnection(this); (getDestination()).removeTargetConnection(this); graphModel.removeConnection(this); if (sourceContainerConnectionFigure != null) { sourceContainerConnectionFigure.getParent().remove(sourceContainerConnectionFigure); } if (targetContainerConnectionFigure != null) { targetContainerConnectionFigure.getParent().remove(targetContainerConnectionFigure); } } public boolean isDisposed() { return isDisposed; } public Connection getConnectionFigure() { if (connectionFigure == null) { connectionFigure = createFigure(); } return connectionFigure; } /** * Gets a proxy to this connection that can be used with the Zest layout * engine * * @return */ public LayoutRelationship getLayoutRelationship() { return this.layoutConnection; } /** * Gets the external connection object. * * @return Object */ public Object getExternalConnection() { return this.getData(); } /** * Returns a string like 'source -> destination' * * @return String */ public String toString() { String arrow = (isBidirectionalInLayout() ? " <--> " : " --> "); String src = (sourceNode != null ? sourceNode.getText() : "null"); String dest = (destinationNode != null ? destinationNode.getText() : "null"); String weight = " (weight=" + getWeightInLayout() + ")"; return ("GraphModelConnection: " + src + arrow + dest + weight); } /** * Returns the style of this connection. Valid styles are those that begin * with CONNECTION in ZestStyles. * * @return the style of this connection. * @see #ZestStyles */ public int getConnectionStyle() { return connectionStyle; } /** * Returns the style of this connection. Valid styles are those that begin * with CONNECTION in ZestStyles. * * @return the style of this connection. * @see #ZestStyles */ public void setConnectionStyle(int style) { this.connectionStyle = style; updateFigure(this.connectionFigure); } /** * Gets the weight of this connection. The weight must be in {-1, [0-1]}. A * weight of -1 means that there is no force/tension between the nodes. A * weight of 0 results in the maximum spring length being used (farthest * apart). A weight of 1 results in the minimum spring length being used * (closest together). * * @see org.eclipse.mylar.zest.layouts.LayoutRelationship#getWeightInLayout() * @return the weight: {-1, [0 - 1]}. */ public double getWeightInLayout() { return weight; } /** * Gets the font for the label on this connection * * @return */ public Font getFont() { return this.font; } /** * Sets the font for the label on this connection. * */ public void setFont(Font f) { this.font = f; } /** * Sets the weight for this connection. The weight must be in {-1, [0-1]}. A * weight of -1 means that there is no force/tension between the nodes. A * weight of 0 results in the maximum spring length being used (farthest * apart). A weight of 1 results in the minimum spring length being used * (closest together). * */ public void setWeight(double weight) { if (weight < 0) { this.weight = -1; } else if (weight > 1) { this.weight = 1; } else { this.weight = weight; } } /** * Returns the color of this connection. * * @return Color */ public Color getLineColor() { return color; } /** * Sets the highlight color. * * @param color * the color to use for highlighting. */ public void setHighlightColor(Color color) { this.highlightColor = color; } /** * @return the highlight color */ public Color getHighlightColor() { return highlightColor; } /** * Perminently sets the color of this line to the given color. This will * become the color of the line when it is not highlighted. If you would * like to temporarily change the color of the line, use changeLineColor. * * @param color * the color to be set. * @see changeLineColor(Color color) */ public void setLineColor(Color color) { this.foreground = color; changeLineColor(foreground); } /** * Sets the connection color. * * @param color */ public void changeLineColor(Color color) { this.color = color; updateFigure(connectionFigure); } /** * Sets the tooltip on this node. This tooltip will display if the mouse * hovers over the node. Setting the tooltip has no effect if a custom * figure has been set. */ public void setTooltip(IFigure tooltip) { hasCustomTooltip = true; this.tooltip = tooltip; updateFigure(connectionFigure); } /** * Gets the current tooltip for this node. The tooltip returned is * meaningless if a custom figure has been set. */ public IFigure getTooltip() { return this.tooltip; } /** * Returns the connection line width. * * @return int */ public int getLineWidth() { return lineWidth; } /** * Sets the connection line width. * * @param lineWidth */ public void setLineWidth(int lineWidth) { this.lineWidth = lineWidth; updateFigure(connectionFigure); } /** * Returns the connection line style. * * @return int */ public int getLineStyle() { return lineStyle; } /** * Sets the connection line style. * * @param lineStyle */ public void setLineStyle(int lineStyle) { this.lineStyle = lineStyle; updateFigure(connectionFigure); } /** * Gets the source node for this relationship * * @return GraphModelNode */ public GraphNode getSource() { return this.sourceNode; } /** * Gets the target node for this relationship * * @return GraphModelNode */ public GraphNode getDestination() { return this.destinationNode; } /** * Highlights this node. Uses the default highlight color. */ public void highlight() { if (highlighted) { return; } highlighted = true; updateFigure(connectionFigure); graphModel.highlightEdge(this); } /** * Unhighlights this node. Uses the default color. */ public void unhighlight() { if (!highlighted) { return; } highlighted = false; updateFigure(connectionFigure); graphModel.unhighlightEdge(this); } /** * Returns true if this connection is highlighted, false otherwise * * @return */ public boolean isHighlighted() { return highlighted; } /** * Gets the graph model that this connection is in * * @return The graph model that this connection is contained in */ public Graph getGraphModel() { return this.graphModel; } /** * Sets the curve depth of the arc. The curve depth is defined as * the maximum distance from any point on the chord (i.e. a vector * normal to the chord with magnitude d). * * If 0 is set, a Polyline Connection will be used, otherwise a * PolylineArcConnectoin will be used. Negative depths are also supported. * @param depth The depth of the curve */ public void setCurveDepth(int depth) { if (this.curveDepth == 0 && depth != 0 || this.curveDepth != 0 && depth == 0) { // There is currently no curve, so we have to create // a curved connection graphModel.removeConnection(this); this.curveDepth = depth; this.connectionFigure = createFigure(); registerConnection(sourceNode, destinationNode); updateFigure(this.connectionFigure); } else { this.curveDepth = depth; updateFigure(this.connectionFigure); } } /* * (non-Javadoc) * * @see org.eclipse.mylar.zest.core.widgets.IGraphItem#getItemType() */ public int getItemType() { return CONNECTION; } /* * (non-Javadoc) * * @see org.eclipse.mylar.zest.core.internal.graphmodel.GraphItem#setVisible(boolean) */ public void setVisible(boolean visible) { //graphModel.addRemoveFigure(this, visible); if (getSource().isVisible() && getDestination().isVisible() && visible) { this.getFigure().setVisible(visible); if (sourceContainerConnectionFigure != null) { sourceContainerConnectionFigure.setVisible(visible); } if (targetContainerConnectionFigure != null) { targetContainerConnectionFigure.setVisible(visible); } this.visible = visible; } else { this.getFigure().setVisible(false); if (sourceContainerConnectionFigure != null) { sourceContainerConnectionFigure.setVisible(false); } if (targetContainerConnectionFigure != null) { targetContainerConnectionFigure.setVisible(false); } this.visible = false; } } /* * (non-Javadoc) * * @see org.eclipse.mylar.zest.core.widgets.IGraphItem#isVisible() */ public boolean isVisible() { return visible; } /* * (non-Javadoc) * * @see org.eclipse.swt.widgets.Item#setText(java.lang.String) */ public void setText(String string) { super.setText(string); if (this.connectionFigure != null) { updateFigure(this.connectionFigure); } } PolylineConnection getSourceContainerConnectionFigure() { return (PolylineConnection) sourceContainerConnectionFigure; } PolylineConnection getTargetContainerConnectionFigure() { return (PolylineConnection) targetContainerConnectionFigure; } private void updateFigure(Connection connection) { if (sourceContainerConnectionFigure != null) { doUpdateFigure(sourceContainerConnectionFigure); } if (targetContainerConnectionFigure != null) { doUpdateFigure(targetContainerConnectionFigure); } doUpdateFigure(connection); } private void doUpdateFigure(Connection connection) { if (connection == null) { return; } Shape connectionShape = (Shape) connection; connectionShape.setLineStyle(getLineStyle()); if (this.getText() != null || this.getImage() != null) { //Label l = new Label(this.getText(), this.getImage()); if (this.getImage() != null) { this.connectionLabel.setIcon(this.getImage()); } if (this.getText() != null) { this.connectionLabel.setText(this.getText()); } this.connectionLabel.setFont(this.getFont()); } if (highlighted) { connectionShape.setForegroundColor(getHighlightColor()); connectionShape.setLineWidth(getLineWidth() * 2); } else { connectionShape.setForegroundColor(getLineColor()); connectionShape.setLineWidth(getLineWidth()); } if (connection instanceof PolylineArcConnection) { PolylineArcConnection arcConnection = (PolylineArcConnection) connection; arcConnection.setDepth(curveDepth); } if ((connectionStyle & ZestStyles.CONNECTIONS_DIRECTED) > 0) { PolygonDecoration decoration = new PolygonDecoration(); if (getLineWidth() < 3) { decoration.setScale(9, 3); } else { double logLineWith = getLineWidth() / 2.0; decoration.setScale(7 * logLineWith, 3 * logLineWith); } ((PolylineConnection) connection).setTargetDecoration(decoration); } IFigure toolTip; if (this.getTooltip() == null && getText() != null && getText().length() > 0 && hasCustomTooltip == false) { toolTip = new Label(); ((Label) toolTip).setText(getText()); } else { toolTip = this.getTooltip(); } connection.setToolTip(toolTip); } private Connection createFigure() { /* if ((sourceNode.getParent()).getItemType() == GraphItem.CONTAINER) { GraphContainer container = (GraphContainer) sourceNode.getParent(); sourceContainerConnectionFigure = doCreateFigure(); container.addConnectionFigure((PolylineConnection) sourceContainerConnectionFigure); } if ((destinationNode.getParent()).getItemType() == GraphItem.CONTAINER) { GraphContainer container = (GraphContainer) destinationNode.getParent(); targetContainerConnectionFigure = doCreateFigure(); container.addConnectionFigure((PolylineConnection) targetContainerConnectionFigure); } */ return doCreateFigure(); } private Connection doCreateFigure() { Connection connectionFigure = null; ChopboxAnchor sourceAnchor = null; ChopboxAnchor targetAnchor = null; this.connectionLabel = new Label(); Locator labelLocator = null; if (getSource() == getDestination()) { // If this is a self loop, create a looped arc and put the locator at the top // of the connection connectionFigure = new PolylineArcConnection(); sourceAnchor = new LoopAnchor(getSource().getNodeFigure()); targetAnchor = new LoopAnchor(getDestination().getNodeFigure()); labelLocator = new MidpointLocator(connectionFigure, 0) { protected Point getReferencePoint() { Point p = Point.SINGLETON; p.x = getConnection().getPoints().getPoint(getIndex()).x; p.y = (int) (getConnection().getPoints().getPoint(getIndex()).y - (curveDepth * 1.5)); return p; } }; } else { if (curveDepth != 0) { connectionFigure = new PolylineArcConnection(); ((PolylineArcConnection) connectionFigure).setDepth(this.curveDepth); } else { connectionFigure = new PolylineConnection(); } sourceAnchor = new RoundedChopboxAnchor(getSource().getNodeFigure(), 8); targetAnchor = new RoundedChopboxAnchor(getDestination().getNodeFigure(), 8); labelLocator = new MidpointLocator(connectionFigure, 0); } connectionFigure.setSourceAnchor(sourceAnchor); connectionFigure.setTargetAnchor(targetAnchor); connectionFigure.add(this.connectionLabel, labelLocator); doUpdateFigure(connectionFigure); return connectionFigure; } /* * (non-Javadoc) * * @see org.eclipse.mylar.zest.layouts.LayoutRelationship#isBidirectionalInLayout() */ private boolean isBidirectionalInLayout() { return !ZestStyles.checkStyle(connectionStyle, ZestStyles.CONNECTIONS_DIRECTED); } class GraphLayoutConnection implements LayoutRelationship { Object layoutInformation = null; public void clearBendPoints() { // @tag TODO : add bendpoints } public LayoutEntity getDestinationInLayout() { return getDestination().getLayoutEntity(); } public Object getLayoutInformation() { return layoutInformation; } public LayoutEntity getSourceInLayout() { return getSource().getLayoutEntity(); } public void populateLayoutConstraint(LayoutConstraint constraint) { graphModel.invokeConstraintAdapters(GraphConnection.this, constraint); } public void setBendPoints(LayoutBendPoint[] bendPoints) { // @tag TODO : add bendpoints } public void setLayoutInformation(Object layoutInformation) { this.layoutInformation = layoutInformation; } public Object getGraphData() { return GraphConnection.this; } public void setGraphData(Object o) { } } IFigure getFigure() { return this.getConnectionFigure(); } }