/* * 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); } } }