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