/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.gui.workflow.editor.connections;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.eclipse.draw2d.ColorConstants;
import org.eclipse.draw2d.Figure;
import org.eclipse.draw2d.FigureCanvas;
import org.eclipse.draw2d.MouseEvent;
import org.eclipse.draw2d.MouseListener;
import org.eclipse.draw2d.PolylineConnection;
import org.eclipse.draw2d.PolylineDecoration;
import org.eclipse.draw2d.XYLayout;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.gef.commands.CommandStack;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.TreeItem;
import de.rcenvironment.core.component.workflow.model.api.Connection;
import de.rcenvironment.core.component.workflow.model.api.WorkflowDescription;
import de.rcenvironment.core.gui.workflow.editor.commands.ConnectionDeleteCommand;
/**
* Composite that displays the connections.
*
* @author Heinrich Wendel
* @author Christian Weiss
* @author Oliver Seebach
*/
public class ConnectionCanvas extends FigureCanvas {
protected static final int DEFAULT_TOLERANCE = 5;
/** The root figure. */
private Figure parentFigure;
/** The context menu. */
private Menu contextMenu;
/** Delete menu item. */
private MenuItem deleteMenuItem;
/** Currently selected Figure. */
private Set<ConnectionFigure> currentSelection = new HashSet<ConnectionFigure>();
/** The associated WorkflowDescription. */
private WorkflowDescription description;
/** TreeViewer on the left of the canvas. */
private EndpointTreeViewer sourceTreeViewer;
/** TreeViewer on the right of the canvas. */
private EndpointTreeViewer targetTreeViewer;
/** TreeViewer on the right of the canvas. */
private CommandStack editorsCommandStack;
/**
* Constructor.
*
* @param parent Cf. parent {@link FigureCanvas#FigureCanvas(Composite, int)}.
* @param style Cf. parent {@link FigureCanvas#FigureCanvas(Composite, int)}.
*/
public ConnectionCanvas(Composite parent, int style) {
super(parent, style);
// create canvas
parentFigure = new Figure();
parentFigure.setLayoutManager(new XYLayout());
setContents(parentFigure);
// add a MouseListener to handle the selection of connections
parentFigure.addMouseListener(new MouseListener() {
public void mouseDoubleClicked(MouseEvent e) {
//
}
public void mousePressed(MouseEvent e) {
if (e.button == 1) {
// clear the previous selection
setSelection(null, (e.getState() & SWT.CONTROL) != 0);
// gather the connections indicated with the mouse click
Set<ConnectionFigure> selection = findConnectionsAt(e.x, e.y, DEFAULT_TOLERANCE);
// set the new selection
setSelection(selection, (e.getState() & SWT.CONTROL) != 0);
// enable the 'delete' menu item, if connections are selected, disable otherwise
deleteMenuItem.setEnabled(!selection.isEmpty());
}
}
public void mouseReleased(MouseEvent e) {
//
}
});
// add a KeyListener to handle deletion of connections
this.addKeyListener(new KeyListener() {
@Override
public void keyReleased(KeyEvent e) {
// if DEL was pressed delete the currently selected connections
if (e.character == SWT.DEL) {
deleteSelectedConnections();
}
}
@Override
public void keyPressed(KeyEvent e) {
//
}
});
// create the context menu
contextMenu = new Menu(this);
setMenu(contextMenu);
deleteMenuItem = new MenuItem(contextMenu, SWT.NONE);
deleteMenuItem.setEnabled(false);
deleteMenuItem.setText(Messages.delete);
// add a SelectionListener to the 'delete' menu item to handle deletion of connections
deleteMenuItem.addSelectionListener(new SelectionListener() {
public void widgetDefaultSelected(SelectionEvent e) {
//
}
public void widgetSelected(SelectionEvent e) {
deleteSelectedConnections();
}
});
}
private void setSelection(final Set<ConnectionFigure> connections, boolean controlDown) {
// clear the former selection
if (!controlDown) {
for (ConnectionFigure connection : currentSelection) {
connection.setSelected(false);
}
currentSelection.clear();
}
// null indicates a request to just clear the current selection, so return early
if (connections != null) {
if (controlDown) {
List<ConnectionFigure> toDelete = new LinkedList<ConnectionFigure>();
for (ConnectionFigure c : connections) {
if (currentSelection.contains(c)) {
toDelete.add(c);
} else {
currentSelection.add(c);
c.setSelected(true);
}
}
for (ConnectionFigure c : toDelete) {
currentSelection.remove(c);
c.setSelected(false);
}
} else {
currentSelection.addAll(connections);
// highlight the new selection
for (ConnectionFigure connection : currentSelection) {
connection.setSelected(true);
}
}
}
}
private Set<ConnectionFigure> getSelection() {
return Collections.unmodifiableSet(currentSelection);
}
public void setEditorsCommandStack(CommandStack editorsCommandStack) {
this.editorsCommandStack = editorsCommandStack;
}
private void deleteSelectedConnections() {
// delete the currently selected connections
deleteConnections(getSelection());
// clear the selection
setSelection(null, false);
}
private void deleteConnections(Collection<ConnectionFigure> connections) {
boolean dirty = false;
List<Connection> connectionsToDelete = new ArrayList<>();
for (ConnectionFigure connectionFigure : connections) {
connectionsToDelete.add(connectionFigure.getConnection());
dirty = true;
}
if (editorsCommandStack == null) {
description.removeConnections(connectionsToDelete);
targetTreeViewer.refresh();
sourceTreeViewer.refresh();
} else {
ConnectionDeleteCommand connectionDeleteCommand = new ConnectionDeleteCommand(description, connectionsToDelete);
editorsCommandStack.execute(connectionDeleteCommand);
}
if (dirty){
repaint();
}
}
/**
* Returns the 'first' {@link ConnectionFigure} at the given coordinates, using the tolerance as
* a 'growing tolerance window'.
*
* @param x the x coordinate
* @param y the y coordinate
* @param tolerance the maximum value for the tolerance window
* @return the 'first' {@link ConnectionFigure} at the given coordinates
*/
protected ConnectionFigure findFirstConnectionAt(final int x, final int y, final int tolerance) {
final List<?> children = parentFigure.getChildren();
// grow the 'tolerance window' from '0' to the provided value
// as soon as connections lie within the tolerance window search is over, thus only the
// 'closest' connections are selected
for (int toleranceIndex = 0; toleranceIndex <= tolerance; ++toleranceIndex) {
final Rectangle hitarea = new Rectangle(x - toleranceIndex, y - toleranceIndex, 1 + 2 * toleranceIndex, 1 + 2 * toleranceIndex);
for (Object child : children) {
if (child instanceof ConnectionFigure) {
ConnectionFigure childFigure = (ConnectionFigure) child;
if (childFigure.intersects(hitarea)) {
return childFigure;
}
}
}
}
return null;
}
/**
* Returns the 'closest' {@link ConnectionFigure}s at the given coordinates, using the tolerance
* as a 'growing tolerance window'.
*
* @param x the x coordinate
* @param y the y coordinate
* @param tolerance the maximum value for the tolerance window
* @return the 'closest' {@link ConnectionFigure}s at the given coordinates
*/
protected Set<ConnectionFigure> findConnectionsAt(final int x, final int y, final int tolerance) {
Set<ConnectionFigure> result = new HashSet<ConnectionFigure>();
List<?> children = parentFigure.getChildren();
// grow the 'tolerance window' from '0' to the provided value
// as soon as connections lie within the tolerance window search is over, thus only the
// 'closest' connections are selected
for (int toleranceIndex = 0; toleranceIndex <= tolerance; ++toleranceIndex) {
final Rectangle hitarea = new Rectangle(x - toleranceIndex, y - toleranceIndex, 1 + 2 * toleranceIndex, 1 + 2 * toleranceIndex);
for (Object child : children) {
if (child instanceof ConnectionFigure) {
ConnectionFigure childFigure = (ConnectionFigure) child;
if (childFigure.intersects(hitarea)) {
result.add(childFigure);
}
}
}
// return as soon as a set of connections with the minimum distance is found
if (!result.isEmpty()) {
break;
}
}
return Collections.unmodifiableSet(result);
}
/**
* Must be called to initialize this view.
*
* @param desc The associated WorkflowDescription.
* @param source The source tree viewer.
* @param target The target tree viewer.
*/
public void initialize(WorkflowDescription desc, EndpointTreeViewer source, EndpointTreeViewer target) {
this.description = desc;
this.sourceTreeViewer = source;
this.targetTreeViewer = target;
}
/**
* Updates the workflow description and repaints.
*
* @param desc The new workflow description
*/
public void updateCanvas(WorkflowDescription desc){
this.description = desc;
repaint();
}
/**
* Repaints the connections on the canvas.
*/
public void repaint() {
// clear the parent figure
parentFigure.removeAll();
// for each connection create a connection figure and add it to the parent figure
for (Connection c : description.getConnections()) {
TreeItem outputItem = sourceTreeViewer.findEndpoint(c.getSourceNode(), c.getOutput().getName());
TreeItem inputItem = targetTreeViewer.findEndpoint(c.getTargetNode(), c.getInput().getName());
// calculate the coordinates of the connection figure (the line) on the canvas
if (inputItem != null && outputItem != null) {
int outputY = outputItem.getBounds().y + outputItem.getBounds().height / 2;
int outputX = 0;
int inputY = inputItem.getBounds().y + inputItem.getBounds().height / 2;
int inputX = parentFigure.getBounds().width;
// create the connection figure (the connection line)
ConnectionFigure line = new ConnectionFigure(c, new Point(outputX, outputY), new Point(inputX, inputY));
// add the connection figure to the parent figure
line.setAntialias(SWT.ON);
parentFigure.add(line);
}
}
}
/**
* PolylineFigure that represents a connection (line).
*
* @author Heinrich Wendel
* @author Christian Weiss
*/
private class ConnectionFigure extends PolylineConnection {
/** The represented {@link Connection} instance. */
private final Connection connection;
/**
* Constructor.
*
* @param connection The connection.
*/
ConnectionFigure(Connection connection, Point start, Point end) {
this.connection = connection;
setStart(start);
setEnd(end);
setTargetDecoration(new PolylineDecoration());
}
/**
* Returns the represented {@link Connection} instance.
*
* @return The represented {@link Connection} instance.
*/
public Connection getConnection() {
return connection;
}
/**
* Sets the selection state of the connection figure.
*
* @param selected The new selection state.
*/
public void setSelected(boolean selected) {
if (selected) {
setForegroundColor(ColorConstants.blue);
} else {
setForegroundColor(ColorConstants.black);
}
}
@Override
public boolean intersects(Rectangle rect) {
if (!super.intersects(rect)) {
return false;
}
int lineWidth = getLineWidth();
int lineWidthAdjustment = lineWidth - 1;
Point start = getStart();
Point end = getEnd();
Line2D line = new Line2D.Float(start.x, start.y, end.x, end.y);
Rectangle2D rectangle =
new Rectangle2D.Double(rect.x - lineWidthAdjustment, rect.y - lineWidthAdjustment, rect.width + 2 * lineWidthAdjustment,
rect.height + 2 * lineWidthAdjustment);
return line.intersects(rectangle);
}
}
}