/*******************************************************************************
* Copyright (c) 2007, 2010 Association for Decentralized Information Management
* in Industry THTH ry.
* 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:
* VTT Technical Research Centre of Finland - initial API and implementation
*******************************************************************************/
package org.simantics.diagram.participant;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import org.simantics.Simantics;
import org.simantics.db.ReadGraph;
import org.simantics.db.Resource;
import org.simantics.db.WriteGraph;
import org.simantics.db.common.request.UniqueRead;
import org.simantics.db.common.request.WriteRequest;
import org.simantics.db.common.utils.NameUtils;
import org.simantics.db.exception.DatabaseException;
import org.simantics.diagram.connection.RouteGraph;
import org.simantics.diagram.connection.RouteGraphConnectionClass;
import org.simantics.diagram.connection.RouteTerminal;
import org.simantics.diagram.connection.rendering.arrows.PlainLineEndStyle;
import org.simantics.diagram.content.ResourceTerminal;
import org.simantics.diagram.stubs.DiagramResource;
import org.simantics.diagram.synchronization.ISynchronizationContext;
import org.simantics.diagram.synchronization.SynchronizationHints;
import org.simantics.g2d.canvas.ICanvasContext;
import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;
import org.simantics.g2d.canvas.impl.DependencyReflection.Reference;
import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;
import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit;
import org.simantics.g2d.connection.IConnectionAdvisor;
import org.simantics.g2d.diagram.DiagramHints;
import org.simantics.g2d.diagram.DiagramUtils;
import org.simantics.g2d.diagram.IDiagram;
import org.simantics.g2d.diagram.handler.Topology.Terminal;
import org.simantics.g2d.diagram.participant.ElementPainter;
import org.simantics.g2d.diagram.participant.TerminalPainter;
import org.simantics.g2d.diagram.participant.TerminalPainter.TerminalHoverStrategy;
import org.simantics.g2d.diagram.participant.pointertool.AbstractMode;
import org.simantics.g2d.diagram.participant.pointertool.PointerInteractor;
import org.simantics.g2d.diagram.participant.pointertool.TerminalUtil;
import org.simantics.g2d.diagram.participant.pointertool.TerminalUtil.TerminalInfo;
import org.simantics.g2d.element.ElementClass;
import org.simantics.g2d.element.ElementClasses;
import org.simantics.g2d.element.ElementUtils;
import org.simantics.g2d.element.IElement;
import org.simantics.g2d.element.IElementClassProvider;
import org.simantics.g2d.element.handler.EdgeVisuals.EdgeEnd;
import org.simantics.g2d.element.handler.SceneGraph;
import org.simantics.g2d.element.handler.TerminalTopology;
import org.simantics.g2d.element.impl.Element;
import org.simantics.g2d.elementclass.BranchPoint;
import org.simantics.g2d.elementclass.BranchPoint.Direction;
import org.simantics.g2d.elementclass.FlagClass;
import org.simantics.g2d.elementclass.FlagHandler;
import org.simantics.g2d.participant.RenderingQualityInteractor;
import org.simantics.g2d.participant.TransformUtil;
import org.simantics.modeling.ModelingResources;
import org.simantics.scenegraph.INode;
import org.simantics.scenegraph.g2d.G2DParentNode;
import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler;
import org.simantics.scenegraph.g2d.events.KeyEvent;
import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
import org.simantics.scenegraph.g2d.events.MouseEvent;
import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonEvent;
import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
import org.simantics.scenegraph.g2d.events.command.CommandEvent;
import org.simantics.scenegraph.g2d.events.command.Commands;
import org.simantics.scenegraph.g2d.nodes.BranchPointNode;
import org.simantics.scenegraph.g2d.nodes.ShapeNode;
import org.simantics.scenegraph.g2d.snap.ISnapAdvisor;
import org.simantics.scenegraph.utils.GeometryUtils;
import org.simantics.scenegraph.utils.Quality;
import org.simantics.structural2.modelingRules.ConnectionJudgement;
import org.simantics.utils.datastructures.Callback;
import org.simantics.utils.datastructures.Pair;
import org.simantics.utils.logging.TimeLogger;
import org.simantics.utils.ui.ErrorLogger;
import org.simantics.utils.ui.ExceptionUtils;
/**
* A basic tool for making connection on diagrams.
*
* This version defines the starting, ending and route points of a connection.
* The routing itself is left up to the diagram router employed by
* {@link DiagramUtils#validateAndFix(IDiagram, ICanvasContext)}.
*
* Manual:
*
* This tool is added to the diagram when a connection sequence is initiated by
* another participant. PointerInteractor is one such participant which adds the
* tool when a terminal or non-terminal-occupied canvas space is ALT+clicked
* (see {@link PointerInteractor#checkInitiateConnectTool(MouseEvent, Point2D)}
* ). The connection will be finished when another allowed terminal is clicked
* upon or empty canvas space is ALT+clicked. Route points for the connection
* can be created by clicking around on non-terminal-occupied canvas space while
* connecting.
*
*
* Connections can be started from and ended in flags by pressing ALT while
* left-clicking.
*
* @author Tuukka Lehtonen
*/
public class ConnectTool2 extends AbstractMode {
public static final int PAINT_PRIORITY = ElementPainter.ELEMENT_PAINT_PRIORITY + 5;
@Reference
protected RenderingQualityInteractor quality;
@Dependency
protected TransformUtil util;
@Dependency
protected ElementPainter diagramPainter;
@Dependency
protected PointerInteractor pi;
/**
* Start element terminal of the connection. null
if connection
* was started from a flag or a branch point.
*
* The value is received by the constructor.
*/
protected List startTerminals;
/**
* Refers to any of the possible overlapping start terminals. The value is
* taken from the first index of {@link #startTerminals} assuming that the
* first one is the nearest. It is null
if
* {@link #startTerminals} is empty.
*/
protected TerminalInfo startTerminal;
protected TerminalInfo startFlag;
/**
* Starting position of the connection, received as an external argument.
*/
protected final Point2D startPos;
/**
* true
if this tool should create connection continuation
* flags, false
otherwise.
*/
protected boolean createFlags;
/**
*
*/
protected IElementClassProvider elementClassProvider;
/**
*
*/
protected Deque controlPoints = new ArrayDeque();
/**
* Contains null
when a connection is started from a new flag
* or one of the terminals in {@link #startTerminals} when a connection is
* being created starting from a terminal or possibly a set of terminals.
*
*
* Note that this is different from {@link #startTerminal} which simply
* represents the first element of {@link #startTerminals}.
*
*
* Only when this value and {@link #endTerminal} is properly set will a
* connection be created between two element terminals.
*/
protected TerminalInfo selectedStartTerminal;
/**
* Element terminal of connection end element. null
if
* connection cannot be ended where it is currently being attempted to end.
*/
protected TerminalInfo endTerminal;
/**
* The latest connectability judgment from the active
* {@link IConnectionAdvisor} should the connection happen between
* {@link #selectedStartTerminal} and {@link #endTerminal}.
*/
protected ConnectionJudgement connectionJudgment;
/**
* If non-null during connection drawing this field tells the direction
* forced for the current branch point by the user through the UI commands
* {@link Commands#ROTATE_ELEMENT_CCW} and
* {@link Commands#ROTATE_ELEMENT_CW}.
*/
private Direction forcedBranchPointDirection;
/**
* A temporary variable for use with
* {@link TerminalTopology#getTerminals(IElement, Collection)}.
*/
protected Collection terminals = new ArrayList();
/**
* Previous mouse canvas position recorded by
* {@link #processMouseMove(MouseMovedEvent)}.
*/
protected Point2D lastMouseCanvasPos = new Point2D.Double();
/**
* Set to true once {@link #processMouseMove(MouseMovedEvent)} has been
* invoked at least once. This is used to tell whether to allow creation of
* branch points or finising the connection in thin air. It will not be
* allowed if the mouse has not moved at all since starting the connection.
*/
protected boolean mouseHasMoved = false;
protected TerminalHoverStrategy originalStrategy = null;
protected TerminalHoverStrategy terminalHoverStrategy = new TerminalHoverStrategy() {
@Override
public boolean highlightEnabled() {
return !isEndingInFlag();
}
@Override
public boolean highlight(TerminalInfo ti) {
boolean reflexive = isStartTerminal(ti.e, ti.t);
if (reflexive && !allowReflexiveConnections())
return false;
return canConnect(ti.e, ti.t) != null;
}
};
protected final static Composite ALPHA_COMPOSITE = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.75f);
/**
* Root scene graph node for all visualization performed by this tool.
*/
protected G2DParentNode ghostNode;
/**
* Indicates whether the connection is about to be ended into a new
* flag/branchpoint or not.
*/
protected TerminalInfo endFlag;
protected G2DParentNode endFlagNode;
/**
* @param startTerminal
* @param mouseId
* @param startCanvasPos
*/
public ConnectTool2(TerminalInfo startTerminal, int mouseId, Point2D startCanvasPos) {
this(startTerminal == null ? Collections. emptyList()
: Collections.singletonList(startTerminal),
mouseId,
startCanvasPos);
}
/**
* @param startTerminals
* @param mouseId
* @param startCanvasPos
*/
public ConnectTool2(List startTerminals, int mouseId, Point2D startCanvasPos) {
super(mouseId);
if (startCanvasPos == null)
throw new NullPointerException("null start position");
if (startTerminals == null)
throw new NullPointerException("null start terminals");
this.startPos = startCanvasPos;
this.lastMouseCanvasPos.setLocation(startPos);
this.startTerminals = startTerminals;
this.startTerminal = startTerminals.isEmpty() ? null : startTerminals.get(0);
}
@Override
public void addedToContext(ICanvasContext ctx) {
super.addedToContext(ctx);
if (quality != null)
quality.setStaticQuality(Quality.LOW);
// Force terminals to always be highlighted without pressing certain
// keys or key combinations.
originalStrategy = getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY);
setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy);
}
@Override
protected void onDiagramSet(IDiagram newDiagram, IDiagram oldDiagram) {
if (newDiagram != null) {
// Get IElementClassProvider
ISynchronizationContext ctx = newDiagram.getHint(SynchronizationHints.CONTEXT);
if (ctx != null) {
this.elementClassProvider = ctx.get(SynchronizationHints.ELEMENT_CLASS_PROVIDER);
}
// See if flags should be created or not.
this.createFlags = Boolean.TRUE.equals(newDiagram.getHint(DiagramHints.KEY_USE_CONNECTION_FLAGS));
startConnection();
}
}
@Override
public void removedFromContext(ICanvasContext ctx) {
if (getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY) == terminalHoverStrategy) {
if (originalStrategy != null)
setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, originalStrategy);
else
removeHint(TerminalPainter.TERMINAL_HOVER_STRATEGY);
}
if (quality != null)
quality.setStaticQuality(null);
super.removedFromContext(ctx);
}
protected void startConnection() {
Point2D startPos = (Point2D) this.startPos.clone();
ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);
if (snapAdvisor != null)
snapAdvisor.snap(startPos);
// Resolve the first element and terminal of the connection.
ControlPoint start = new ControlPoint(startPos);
if (startTerminal != null) {
assert ElementUtils.peekDiagram(startTerminal.e) == diagram;
Point2D terminalPos = new Point2D.Double(startTerminal.posDia.getTranslateX(),
startTerminal.posDia.getTranslateY());
start.setPosition(terminalPos).setAttachedToTerminal(startTerminal);
} else {
// Create TerminalInfo describing the flag to be created.
if (createFlags) {
// This prevents connection creation from creating a branch
// point in place of this flag.
startFlag = createFlag(EdgeEnd.Begin);
start.setAttachedToTerminal(startFlag);
showElement(ghostNode, "startFlag", startFlag.e, startPos);
}
}
controlPoints.add(start);
controlPoints.add(new ControlPoint(startPos));
// Make sure that we are ending with a flag if ALT is pressed.
// This makes the tool always start with a flag which can be quite
// cumbersome and is therefore disabled. The current version will not
// end the connection if the mouse has not moved at all.
//if (keyUtil.isKeyPressed(java.awt.event.KeyEvent.VK_ALT)) {
// endWithoutTerminal(lastMouseCanvasPos, true);
//}
}
@SGInit
public void initSG(G2DParentNode parent) {
ghostNode = parent.addNode(G2DParentNode.class);
ghostNode.setZIndex(PAINT_PRIORITY);
ShapeNode pathNode = ghostNode.getOrCreateNode("path", ShapeNode.class);
pathNode.setColor(new Color(160, 0, 0));
pathNode.setStroke(new BasicStroke(2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10,
new float[] { 5f, 2f }, 0));
pathNode.setScaleStroke(true);
pathNode.setZIndex(0);
G2DParentNode points = ghostNode.getOrCreateNode("points", G2DParentNode.class);
points.setZIndex(1);
updateSG();
}
static class Segment {
public final ControlPoint begin;
public final ControlPoint end;
public Path2D path;
public Segment(ControlPoint begin, ControlPoint end) {
this.begin = begin;
this.end = end;
}
@Override
public String toString() {
return "Segment[begin=" + begin + ", end=" + end + ", path=" + path + "]";
}
}
private RouteTerminal addControlPoint(RouteGraph routeGraph, ControlPoint cp) {
TerminalInfo ti = cp.getAttachedTerminal();
if(ti != null && ti != startFlag && ti != endFlag) {
Rectangle2D bounds = ElementUtils.getElementBoundsOnDiagram(ti.e, new Rectangle2D.Double());
GeometryUtils.expandRectangle(bounds, 2);
int allowedDirections = RouteGraphConnectionClass.shortestDirectionOutOfBounds(
ti.posDia.getTranslateX(), ti.posDia.getTranslateY(), bounds);
return routeGraph.addTerminal(ti.posDia.getTranslateX(), ti.posDia.getTranslateY(),
bounds, allowedDirections, PlainLineEndStyle.INSTANCE);
}
else {
double x = cp.getPosition().getX();
double y = cp.getPosition().getY();
int allowedDirections = 0xf;
switch(cp.getDirection()) {
case Horizontal: allowedDirections = 5; break;
case Vertical: allowedDirections = 10; break;
case Any: allowedDirections = 15; break;
}
return routeGraph.addTerminal(x, y, x, y, x, y, allowedDirections);
}
}
protected void updateSG() {
if (controlPoints.size() != 2)
return;
ControlPoint begin = controlPoints.getFirst();
ControlPoint end = controlPoints.getLast();
RouteGraph routeGraph = new RouteGraph();
RouteTerminal a = addControlPoint(routeGraph, begin);
RouteTerminal b = addControlPoint(routeGraph, end);
routeGraph.link(a, b);
// Route connection segments separately
/*Router2 router = ElementUtils.getHintOrDefault(diagram, DiagramHints.ROUTE_ALGORITHM, TrivialRouter2.INSTANCE);
final List segments = toSegments(controlPoints);
//System.out.println("controlpoints: " + controlPoints);
//System.out.println("segments: " + segments);
router.route(new IConnection() {
@Override
public Collection extends Object> getSegments() {
return segments;
}
@Override
public Connector getBegin(Object seg) {
return getConnector(((Segment) seg).begin);
}
@Override
public Connector getEnd(Object seg) {
return getConnector(((Segment) seg).end);
}
private Connector getConnector(ControlPoint cp) {
Connector c = new Connector();
c.x = cp.getPosition().getX();
c.y = cp.getPosition().getY();
TerminalInfo ti = cp.getAttachedTerminal();
if (ti != null && (ti == startFlag || ti != endFlag)) {
//System.out.println("CP1: " + cp);
c.parentObstacle = DiagramUtils.getObstacleShape(ti.e);
ConnectionDirectionUtil.determineAllowedDirections(c);
} else {
//System.out.println("CP2: " + cp);
c.parentObstacle = GeometryUtils.transformRectangle(AffineTransform.getTranslateInstance(c.x, c.y),
BranchPointClass.DEFAULT_IMAGE2.getBounds());
c.allowedDirections = toAllowedDirections(cp.getDirection());
}
return c;
}
@Override
public void setPath(Object seg, Path2D path) {
((Segment) seg).path = (Path2D) path.clone();
}
private int toAllowedDirections(BranchPoint.Direction direction) {
switch (direction) {
case Any:
return 0xf;
case Horizontal:
return Constants.EAST_FLAG | Constants.WEST_FLAG;
case Vertical:
return Constants.NORTH_FLAG | Constants.SOUTH_FLAG;
default:
throw new IllegalArgumentException("unrecognized direction: " + direction);
}
}
});
// Combine the routed paths
Path2D path = new Path2D.Double();
for (Segment seg : segments) {
//System.out.println("SEG: " + seg);
if (seg.path != null)
path.append(seg.path.getPathIterator(null), true);
}*/
Path2D path = routeGraph.getPath2D();
// Create scene graph to visualize the connection.
ShapeNode pathNode = ghostNode.getOrCreateNode("path", ShapeNode.class);
pathNode.setShape(path);
G2DParentNode points = ghostNode.getOrCreateNode("points", G2DParentNode.class);
HashSet unusedChildren = new HashSet(points.getNodes());
int i = 0;
for (ControlPoint cp : controlPoints) {
if (cp.isAttachedToTerminal())
continue;
String id = String.valueOf(i);
BranchPointNode bpn = points.getOrCreateNode(id, BranchPointNode.class);
bpn.setDegree(2);
bpn.setDirection((byte) cp.getDirection().ordinal());
bpn.setTransform(AffineTransform.getTranslateInstance(cp.getPosition().getX(), cp.getPosition().getY()));
++i;
unusedChildren.remove(bpn);
}
for (INode unusedChild : unusedChildren)
points.removeNode(unusedChild);
setDirty();
}
private G2DParentNode showElement(G2DParentNode parent, String nodeId, IElement element, Point2D pos) {
return showElement(parent, nodeId, element, AffineTransform.getTranslateInstance(pos.getX(), pos.getY()));
}
private G2DParentNode showElement(G2DParentNode parent, String nodeId, IElement element, AffineTransform tr) {
G2DParentNode elementParent = parent.getOrCreateNode(nodeId, G2DParentNode.class);
elementParent.setTransform(tr);
elementParent.removeNodes();
for (SceneGraph sg : element.getElementClass().getItemsByClass(SceneGraph.class))
sg.init(element, elementParent);
return elementParent;
}
private List toSegments(Deque points) {
if (points.isEmpty())
return Collections.emptyList();
List segments = new ArrayList();
Iterator it = points.iterator();
ControlPoint prev = it.next();
while (it.hasNext()) {
ControlPoint next = it.next();
segments.add(new Segment(prev, next));
prev = next;
}
return segments;
}
@SGCleanup
public void cleanupSG() {
ghostNode.remove();
ghostNode = null;
}
@EventHandler(priority = 200)
public boolean handleCommandEvents(CommandEvent ce) {
if (ce.command.equals(Commands.CANCEL)) {
setDirty();
remove();
return true;
} else if (ce.command.equals(Commands.ROTATE_ELEMENT_CCW) || ce.command.equals(Commands.ROTATE_ELEMENT_CW)) {
return rotateLastBranchPoint(ce.command.equals(Commands.ROTATE_ELEMENT_CW));
}
return false;
}
@EventHandler(priority = PointerInteractor.TOOL_PRIORITY + 20)
public boolean handleKeyEvents(KeyEvent ke) {
if (ke instanceof KeyPressedEvent) {
// Back-space, cancel prev bend
if (ke.keyCode == java.awt.event.KeyEvent.VK_BACK_SPACE)
return cancelPreviousBend();
}
if (ke.keyCode == java.awt.event.KeyEvent.VK_ALT) {
if (createFlags) {
endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(ke instanceof KeyPressedEvent));
return true;
}
}
return false;
}
@EventHandler(priority = PointerInteractor.TOOL_PRIORITY + 20)
public boolean handleEvent(MouseEvent me) {
// Only handle events for the connection-initiating mouse
if (me.mouseId != mouseId)
return false;
if (me instanceof MouseMovedEvent)
return processMouseMove((MouseMovedEvent) me);
if (me instanceof MouseButtonPressedEvent)
return processMouseButtonPress((MouseButtonPressedEvent) me);
return false;
}
protected boolean processMouseMove(MouseMovedEvent me) {
mouseHasMoved = true;
Point2D mouseControlPos = me.controlPosition;
Point2D mouseCanvasPos = util.controlToCanvas(mouseControlPos, new Point2D.Double());
ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);
if (snapAdvisor != null)
snapAdvisor.snap(mouseCanvasPos);
// Record last snapped canvas position of mouse.
this.lastMouseCanvasPos.setLocation(mouseCanvasPos);
if (isEndingInFlag()) {
endFlagNode.setTransform(AffineTransform.getTranslateInstance(mouseCanvasPos.getX(), mouseCanvasPos.getY()));
}
List tis = pi.pickTerminals(me.controlPosition);
tis = TerminalUtil.findNearestOverlappingTerminals(tis);
if (!tis.isEmpty() && !containsStartTerminal(tis)) {
//System.out.println("end terminals (" + tis.size() + "):\n" + EString.implode(tis));
for (TerminalInfo ti : tis) {
Pair canConnect = canConnect(ti.e, ti.t);
if (canConnect != null) {
connectionJudgment = canConnect.first;
if (!isEndingInFlag() || !TerminalUtil.isSameTerminal(ti, endTerminal)) {
if (canConnect.second != null) {
controlPoints.getFirst()
.setPosition(canConnect.second.posDia)
.setAttachedToTerminal(canConnect.second);
}
controlPoints.getLast()
.setPosition(ti.posDia)
.setAttachedToTerminal(ti);
selectedStartTerminal = canConnect.second;
endTerminal = ti;
}
// Make sure that we are ending with a flag if ALT is pressed
// and no end terminal is defined.
endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me));
updateSG();
return false;
}
}
}
connectionJudgment = null;
if (isEndTerminalDefined()) {
// CASE: Mouse was previously on top of a valid terminal to end
// the connection. Now the mouse has been moved where there is
// no longer a terminal to connect to.
//
// => Disconnect the last edge segment from the previous
// terminal, mark endElement/endTerminal non-existent
// and connect the disconnected edge to a new branch point.
controlPoints.getLast()
.setPosition(mouseCanvasPos)
.setDirection(calculateCurrentBranchPointDirection())
.setAttachedToTerminal(null);
endTerminal = null;
} else {
// CASE: Mouse was not previously on top of a valid ending
// element terminal.
//
// => Move and re-orient last branch point.
controlPoints.getLast()
.setPosition(mouseCanvasPos)
.setDirection(calculateCurrentBranchPointDirection());
}
// Make sure that we are ending with a flag if ALT is pressed and no end
// terminal is defined.
endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me));
updateSG();
return false;
}
protected boolean processMouseButtonPress(MouseButtonPressedEvent e) {
MouseButtonEvent me = e;
// Do nothing before the mouse has moved at least a little.
// This prevents the user from ending the connection right where
// it started.
if (!mouseHasMoved)
return true;
if (me.button == MouseEvent.LEFT_BUTTON) {
Point2D mouseControlPos = me.controlPosition;
Point2D mouseCanvasPos = util.getInverseTransform().transform(mouseControlPos, new Point2D.Double());
ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);
if (snapAdvisor != null)
snapAdvisor.snap(mouseCanvasPos);
// Clicked on an allowed end terminal. End connection & end mode.
if (isEndTerminalDefined()) {
createConnection();
remove();
return true;
} else {
// Finish connection in thin air only if the
// connection was started from a valid terminal.
if (me.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK) && !startTerminals.isEmpty()) {
Pair pair = canConnect(null, null);
if (pair != null) {
connectionJudgment = (ConnectionJudgement) pair.first;
selectedStartTerminal = pair.second;
// endFlag = createFlag(EdgeEnd.End);
// controlPoints.getLast().setAttachedToTerminal(endFlag);
createConnection();
setDirty();
remove();
} else {
// Inform the user why connection couldn't be created.
String tmsg = terminalsToString(startTerminals);
ErrorLogger.defaultLogWarning("Can't resolve connection type for new connection when starting from one of the following terminals:\n" + tmsg, null);
}
return true;
} else if (routePointsAllowed()
&& (me.stateMask & (MouseEvent.ALT_MASK | MouseEvent.SHIFT_MASK | MouseEvent.CTRL_MASK)) == 0) {
// Add new connection control point.
controlPoints.add(newControlPointWithCalculatedDirection(mouseCanvasPos));
resetForcedBranchPointDirection();
updateSG();
}
}
// Eat the event to prevent other participants from doing
// incompatible things while in this connection mode.
return true;
} else if (me.button == MouseEvent.RIGHT_BUTTON) {
return cancelPreviousBend();
}
return false;
}
protected boolean cancelPreviousBend() {
if (!routePointsAllowed())
return false;
// Just to make this code more comprehensible, prevent an editing
// case that requires ugly code to work.
if (isEndingInFlag())
return true;
// If there are no real route points, cancel whole connection.
if (controlPoints.size() <= 2) {
setDirty();
remove();
return true;
}
// Cancel last bend
controlPoints.removeLast();
controlPoints.getLast().setPosition(lastMouseCanvasPos);
resetForcedBranchPointDirection();
updateSG();
return true;
}
/**
* Rotates the last branch point in the created connection in either
* clockwise or counter-clockwise direction as a response to a user
* interaction.
*
*
* At the same time it use {@link #forcedBranchPointDirection} to mark the
* current last branch point to be forcefully oriented according to the
* users wishes instead of calculating a default value for the orientation
* from the routed connection path. See
* {@link #calculateCurrentBranchPointDirection()} for more information on
* this.
*
*
* The logic of this method goes as follows:
*
* - Calculate the current branch point direction
* - If the branch point direction is currently user selected (
* {@link #forcedBranchPointDirection}
*
*
*
*
* @param clockwise
* @return true
if the rotation was successful
*/
protected boolean rotateLastBranchPoint(boolean clockwise) {
Direction oldDir = calculateCurrentBranchPointDirection();
if (forcedBranchPointDirection == null) {
forcedBranchPointDirection = oldDir.toggleDetermined();
} else {
forcedBranchPointDirection = clockwise ? oldDir.cycleNext() : oldDir.cyclePrevious();
}
controlPoints.getLast().setDirection(forcedBranchPointDirection);
updateSG();
return true;
}
/**
* Set preferred direction for a branch/route point element.
*
* @param branchPoint the element to set the direction for
* @param direction the direction to set
* @return
*/
protected void setDirection(IElement branchPoint, Direction direction) {
branchPoint.getElementClass().getSingleItem(BranchPoint.class).setDirectionPreference(branchPoint, direction);
}
protected Direction forcedBranchPointDirection() {
return forcedBranchPointDirection;
}
protected void resetForcedBranchPointDirection() {
forcedBranchPointDirection = null;
}
protected void forceBranchPointDirection(Direction direction) {
forcedBranchPointDirection = direction;
}
/**
* @return
*/
protected Direction calculateCurrentBranchPointDirection() {
// If this is not the first branch point, toggle direction compared to
// last.
if (forcedBranchPointDirection != null)
return forcedBranchPointDirection;
if (controlPoints.size() > 2) {
// This is not the first edge segment, toggle route point
// directions.
Iterator it = controlPoints.descendingIterator();
it.next();
ControlPoint secondLastCp = it.next();
Direction dir = secondLastCp.getDirection();
switch (dir) {
case Horizontal:
return Direction.Vertical;
case Vertical:
return Direction.Horizontal;
case Any:
}
}
// If this is the first branch point, calculate based on edge segment
// angle.
if (controlPoints.size() > 1) {
Iterator it = controlPoints.descendingIterator();
ControlPoint last = it.next();
ControlPoint secondLast = it.next();
double angle = Math.atan2(Math.abs(last.getPosition().getY() - secondLast.getPosition().getY()),
Math.abs(last.getPosition().getX() - secondLast.getPosition().getX()));
if (angle >= 0 && angle < Math.PI / 4) {
return Direction.Horizontal;
} else if (angle > Math.PI / 4 && angle <= Math.PI / 2) {
return Direction.Vertical;
}
}
return Direction.Any;
}
protected boolean isEndingInFlag() {
return endFlag != null;
}
protected void endWithoutTerminal(Point2D mousePos, boolean altDown) {
// Just go with branch points if flags are not allowed.
if (!createFlags)
return;
boolean endTerminalDefined = isEndTerminalDefined();
if (altDown) {
if (!isEndingInFlag()) {
endFlag = createFlag(EdgeEnd.End);
endFlagNode = showElement(ghostNode, "endFlag", endFlag.e, mousePos);
controlPoints.getLast()
.setDirection(calculateCurrentBranchPointDirection())
.setAttachedToTerminal(endFlag);
// TerminalPainter must refresh
setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy);
updateSG();
}
} else {
if (isEndingInFlag()) {
// Currently ending with flag but ALT is no longer down
// so that flag must be removed.
endFlag = null;
endFlagNode.remove();
endFlagNode = null;
ControlPoint cp = controlPoints.getLast();
cp.setDirection(calculateCurrentBranchPointDirection())
.setAttachedToTerminal(endTerminal);
if (endTerminalDefined) {
cp.setPosition(endTerminal.posDia);
} else {
cp.setPosition(mousePos);
}
// Force TerminalPainter refresh
setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy);
updateSG();
}
}
}
protected void createConnection() {
createConnection(
this.selectedStartTerminal,
this.endTerminal,
this.connectionJudgment,
this.controlPoints);
}
protected void createConnection(
final TerminalInfo startTerminal,
final TerminalInfo endTerminal,
final ConnectionJudgement judgement,
final Deque controlPoints)
{
TimeLogger.resetTimeAndLog(getClass(), "createConnection");
if (judgement == null) {
// Inform the user why connection couldn't be created.
String tmsg = terminalsToString(Arrays.asList(startTerminal, endTerminal));
ErrorLogger.defaultLogError("Cannot create connection, no judgment available on connection validity when connecting the terminals:\n" + tmsg, null);
return;
}
final ConnectionBuilder builder = new ConnectionBuilder(this.diagram);
Simantics.getSession().asyncRequest(new WriteRequest() {
@Override
public void perform(WriteGraph graph) throws DatabaseException {
builder.create(graph, judgement, controlPoints, startTerminal, endTerminal);
}
}, new Callback() {
@Override
public void run(DatabaseException parameter) {
if (parameter != null)
ExceptionUtils.logAndShowError(parameter);
}
});
}
/**
* @param canvasPos
* @return
*/
protected ControlPoint newControlPointWithCalculatedDirection(Point2D canvasPos) {
return new ControlPoint(canvasPos, calculateCurrentBranchPointDirection());
}
/**
* @param e
* @param t
* @return true
if the specified element terminal matches any
* TerminalInfo in {@link #startTerminals}
*/
protected boolean isStartTerminal(IElement e, Terminal t) {
if (startTerminal == null)
return false;
for (TerminalInfo st : startTerminals) {
if (st.e == e && st.t == t) {
return true;
}
}
return false;
}
/**
* @param e
* @param t
* @return true
if the specified element terminal matches any
* TerminalInfo in {@link #startTerminals}
*/
protected boolean containsStartTerminal(List tis) {
if (startTerminal == null)
return false;
for (TerminalInfo st : startTerminals) {
for (TerminalInfo et : tis) {
if (st.e == et.e && st.t == et.t) {
return true;
}
}
}
return false;
}
protected static FlagClass.Type endToFlagType(EdgeEnd end) {
switch (end) {
case Begin:
return FlagClass.Type.In;
case End:
return FlagClass.Type.Out;
default:
throw new IllegalArgumentException("unrecognized edge end: " + end);
}
}
protected TerminalInfo createFlag(EdgeEnd connectionEnd) {
ElementClass flagClass = elementClassProvider.get(ElementClasses.FLAG);
IElement e = Element.spawnNew(flagClass);
e.setHint(FlagClass.KEY_FLAG_TYPE, endToFlagType(connectionEnd));
e.setHint(FlagClass.KEY_FLAG_MODE, FlagClass.Mode.Internal);
TerminalInfo ti = new TerminalInfo();
ti.e = e;
ti.t = ElementUtils.getSingleTerminal(e);
ti.posElem = TerminalUtil.getTerminalPosOnElement(e, ti.t);
ti.posDia = TerminalUtil.getTerminalPosOnDiagram(e, ti.t);
return ti;
}
protected boolean shouldEndWithFlag(MouseEvent me) {
return shouldEndWithFlag( me.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK) );
}
protected boolean shouldEndWithFlag(boolean altPressed) {
return altPressed && !isEndTerminalDefined() && createFlags && startFlag == null;
}
protected boolean isEndTerminalDefined() {
return endTerminal != null;
}
protected boolean isFlagTerminal(TerminalInfo ti) {
return ti.e.getElementClass().containsClass(FlagHandler.class);
}
protected boolean allowReflexiveConnections() {
return false;
}
protected boolean routePointsAllowed() {
return Boolean.TRUE.equals(diagram.getHint(DiagramHints.KEY_ALLOW_ROUTE_POINTS));
}
/**
* @param endElement
* @param endTerminal
* @return
*/
@SuppressWarnings("unchecked")
protected final Pair canConnect(IElement endElement, Terminal endTerminal) {
IConnectionAdvisor advisor = diagram.getHint(DiagramHints.CONNECTION_ADVISOR);
Object judgement = canConnect(advisor, endElement, endTerminal);
if (judgement == null)
return null;
if (judgement instanceof Pair, ?>)
return (Pair) judgement;
return Pair.make((ConnectionJudgement) judgement, startTerminal);
}
protected Object canConnect(IConnectionAdvisor advisor, IElement endElement, Terminal endTerminal) {
if (advisor == null)
return Pair.make(ConnectionJudgement.CANBEMADELEGAL, startTerminal);
if (startTerminals.isEmpty()) {
ConnectionJudgement obj = (ConnectionJudgement) advisor.canBeConnected(null, null, null, endElement, endTerminal);
return obj != null ? Pair.make(obj, null) : null;
}
for (TerminalInfo st : startTerminals) {
ConnectionJudgement obj = (ConnectionJudgement) advisor.canBeConnected(null, st.e, st.t, endElement, endTerminal);
if (obj != null) {
return Pair.make(obj, st);
}
}
return null;
}
/**
* For generating debugging information of what was attempted by the user
* when a connection couldn't be created.
*
* @param ts
* @return
*/
private String terminalsToString(final Iterable ts) {
try {
return Simantics.sync(new UniqueRead() {
@Override
public String perform(ReadGraph graph) throws DatabaseException {
DiagramResource DIA = DiagramResource.getInstance(graph);
ModelingResources MOD = ModelingResources.getInstance(graph);
StringBuilder sb = new StringBuilder();
boolean first = true;
for (TerminalInfo ti : ts) {
if (!first)
sb.append("\n");
first = false;
sb.append("element ");
Object o = ElementUtils.getObject(ti.e);
if (o instanceof Resource) {
Resource er = (Resource) o;
Resource cer = graph.getPossibleObject(er, MOD.ElementToComponent);
Resource r = cer != null ? cer : er;
sb.append(NameUtils.getSafeName(graph, r)).append(" : ");
for (Resource type : graph.getPrincipalTypes(r)) {
sb.append(NameUtils.getSafeName(graph, type, true));
}
} else {
sb.append(ti.e.toString());
}
sb.append(", terminal ");
if (ti.t instanceof ResourceTerminal) {
Resource tr = ((ResourceTerminal) ti.t).getResource();
Resource cp = graph.getPossibleObject(tr, DIA.HasConnectionPoint);
Resource r = cp != null ? cp : tr;
sb.append(NameUtils.getSafeName(graph, r, true));
} else {
sb.append(ti.t.toString());
}
}
return sb.toString();
}
});
} catch (DatabaseException e) {
return e.getMessage();
}
}
}