1 /*******************************************************************************
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.diagram.participant;
14 import java.awt.AlphaComposite;
15 import java.awt.BasicStroke;
16 import java.awt.Color;
17 import java.awt.Composite;
18 import java.awt.geom.AffineTransform;
19 import java.awt.geom.Path2D;
20 import java.awt.geom.Point2D;
21 import java.awt.geom.Rectangle2D;
22 import java.util.ArrayDeque;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.Deque;
28 import java.util.Iterator;
29 import java.util.List;
31 import org.simantics.Simantics;
32 import org.simantics.db.ReadGraph;
33 import org.simantics.db.Resource;
34 import org.simantics.db.WriteGraph;
35 import org.simantics.db.common.request.UniqueRead;
36 import org.simantics.db.common.request.WriteRequest;
37 import org.simantics.db.common.utils.NameUtils;
38 import org.simantics.db.exception.DatabaseException;
39 import org.simantics.diagram.connection.RouteGraph;
40 import org.simantics.diagram.connection.RouteLine;
41 import org.simantics.diagram.connection.RouteTerminal;
42 import org.simantics.diagram.connection.delta.RouteGraphDelta;
43 import org.simantics.diagram.connection.rendering.arrows.PlainLineEndStyle;
44 import org.simantics.diagram.content.ResourceTerminal;
45 import org.simantics.diagram.stubs.DiagramResource;
46 import org.simantics.diagram.synchronization.ISynchronizationContext;
47 import org.simantics.diagram.synchronization.SynchronizationHints;
48 import org.simantics.diagram.synchronization.graph.RouteGraphConnection;
49 import org.simantics.g2d.canvas.ICanvasContext;
50 import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;
51 import org.simantics.g2d.canvas.impl.DependencyReflection.Reference;
52 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;
53 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit;
54 import org.simantics.g2d.connection.IConnectionAdvisor;
55 import org.simantics.g2d.diagram.DiagramHints;
56 import org.simantics.g2d.diagram.DiagramUtils;
57 import org.simantics.g2d.diagram.IDiagram;
58 import org.simantics.g2d.diagram.handler.PickContext;
59 import org.simantics.g2d.diagram.handler.Topology.Terminal;
60 import org.simantics.g2d.diagram.participant.ElementPainter;
61 import org.simantics.g2d.diagram.participant.TerminalPainter;
62 import org.simantics.g2d.diagram.participant.TerminalPainter.TerminalHoverStrategy;
63 import org.simantics.g2d.diagram.participant.pointertool.AbstractMode;
64 import org.simantics.g2d.diagram.participant.pointertool.PointerInteractor;
65 import org.simantics.g2d.diagram.participant.pointertool.TerminalUtil;
66 import org.simantics.g2d.diagram.participant.pointertool.TerminalUtil.TerminalInfo;
67 import org.simantics.g2d.element.ElementClass;
68 import org.simantics.g2d.element.ElementClasses;
69 import org.simantics.g2d.element.ElementUtils;
70 import org.simantics.g2d.element.IElement;
71 import org.simantics.g2d.element.IElementClassProvider;
72 import org.simantics.g2d.element.handler.EdgeVisuals.EdgeEnd;
73 import org.simantics.g2d.element.handler.SceneGraph;
74 import org.simantics.g2d.element.handler.TerminalTopology;
75 import org.simantics.g2d.element.handler.impl.BranchPointTerminal;
76 import org.simantics.g2d.element.impl.Element;
77 import org.simantics.g2d.elementclass.BranchPoint;
78 import org.simantics.g2d.elementclass.BranchPoint.Direction;
79 import org.simantics.g2d.elementclass.FlagClass;
80 import org.simantics.g2d.elementclass.FlagHandler;
81 import org.simantics.g2d.elementclass.RouteGraphConnectionClass;
82 import org.simantics.g2d.participant.RenderingQualityInteractor;
83 import org.simantics.g2d.participant.TransformUtil;
84 import org.simantics.g2d.utils.geom.DirectionSet;
85 import org.simantics.modeling.ModelingResources;
86 import org.simantics.scenegraph.g2d.G2DParentNode;
87 import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler;
88 import org.simantics.scenegraph.g2d.events.KeyEvent;
89 import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
90 import org.simantics.scenegraph.g2d.events.MouseEvent;
91 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonEvent;
92 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
93 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent;
94 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
95 import org.simantics.scenegraph.g2d.events.command.CommandEvent;
96 import org.simantics.scenegraph.g2d.events.command.Commands;
97 import org.simantics.scenegraph.g2d.nodes.BranchPointNode;
98 import org.simantics.scenegraph.g2d.nodes.ShapeNode;
99 import org.simantics.scenegraph.g2d.snap.ISnapAdvisor;
100 import org.simantics.scenegraph.utils.GeometryUtils;
101 import org.simantics.scenegraph.utils.Quality;
102 import org.simantics.structural2.modelingRules.ConnectionJudgement;
103 import org.simantics.utils.datastructures.Pair;
104 import org.simantics.utils.logging.TimeLogger;
105 import org.simantics.utils.ui.ErrorLogger;
106 import org.simantics.utils.ui.ExceptionUtils;
108 import gnu.trove.map.hash.THashMap;
111 * A basic tool for making connection on diagrams.
113 * This version defines the starting, ending and route points of a connection.
114 * The routing itself is left up to the diagram router employed by
115 * {@link DiagramUtils#validateAndFix(IDiagram, ICanvasContext)}.
119 * This tool is added to the diagram when a connection sequence is initiated by
120 * another participant. PointerInteractor is one such participant which adds the
121 * tool when a terminal or non-terminal-occupied canvas space is ALT+clicked
122 * (see {@link PointerInteractor#checkInitiateConnectTool(MouseEvent, Point2D)}
123 * ). The connection will be finished when another allowed terminal is clicked
124 * upon or empty canvas space is ALT+clicked. Route points for the connection
125 * can be created by clicking around on non-terminal-occupied canvas space while
129 * Connections can be started from and ended in flags by pressing ALT while
132 * @author Tuukka Lehtonen
134 public class ConnectTool2 extends AbstractMode {
136 public static final int PAINT_PRIORITY = ElementPainter.ELEMENT_PAINT_PRIORITY + 5;
139 protected RenderingQualityInteractor quality;
142 protected TransformUtil util;
145 protected ElementPainter diagramPainter;
148 protected PointerInteractor pi;
151 protected PickContext pickContext;
154 * Start element terminal of the connection. <code>null</code> if connection
155 * was started from a flag or a branch point.
157 * The value is received by the constructor.
159 protected List<TerminalInfo> startTerminals;
162 * Refers to any of the possible overlapping start terminals. The value is
163 * taken from the first index of {@link #startTerminals} assuming that the
164 * first one is the nearest. It is <code>null</code> if
165 * {@link #startTerminals} is empty.
167 protected TerminalInfo startTerminal;
169 protected TerminalInfo startFlag;
172 * Starting position of the connection, received as an external argument.
174 protected final Point2D startPos;
177 * <code>true</code> if this tool should create connection continuation
178 * flags, <code>false</code> otherwise.
180 protected boolean createFlags;
185 protected IElementClassProvider elementClassProvider;
190 protected Deque<ControlPoint> controlPoints = new ArrayDeque<ControlPoint>();
193 * Contains <code>null</code> when a connection is started from a new flag
194 * or one of the terminals in {@link #startTerminals} when a connection is
195 * being created starting from a terminal or possibly a set of terminals.
198 * Note that this is different from {@link #startTerminal} which simply
199 * represents the first element of {@link #startTerminals}.
202 * Only when this value and {@link #endTerminal} is properly set will a
203 * connection be created between two element terminals.
205 protected TerminalInfo selectedStartTerminal;
208 * Element terminal of connection end element. <code>null</code> if
209 * connection cannot be ended where it is currently being attempted to end.
211 protected TerminalInfo endTerminal;
214 * The latest connectability judgment from the active
215 * {@link IConnectionAdvisor} should the connection happen between
216 * {@link #selectedStartTerminal} and {@link #endTerminal}.
218 protected ConnectionJudgement connectionJudgment;
221 * The latest connectability judgment from the active
222 * {@link IConnectionAdvisor} should the connection happen between
223 * {@link #selectedStartTerminal} and {@link #lastRouteGraphTarget}.
225 protected ConnectionJudgement attachToConnectionJudgement;
228 * If non-null during connection drawing this field tells the direction
229 * forced for the current branch point by the user through the UI commands
230 * {@link Commands#ROTATE_ELEMENT_CCW} and
231 * {@link Commands#ROTATE_ELEMENT_CW}.
233 private Direction forcedBranchPointDirection;
236 * A temporary variable for use with
237 * {@link TerminalTopology#getTerminals(IElement, Collection)}.
239 protected Collection<Terminal> terminals = new ArrayList<Terminal>();
242 * Previous mouse canvas position recorded by
243 * {@link #processMouseMove(MouseMovedEvent)}.
245 protected Point2D lastMouseCanvasPos = new Point2D.Double();
248 * Set to true once {@link #processMouseMove(MouseMovedEvent)} has been
249 * invoked at least once. This is used to tell whether to allow creation of
250 * branch points or finising the connection in thin air. It will not be
251 * allowed if the mouse has not moved at all since starting the connection.
253 protected boolean mouseHasMoved = false;
255 protected TerminalHoverStrategy originalStrategy = null;
257 protected TerminalHoverStrategy terminalHoverStrategy = new TerminalHoverStrategy() {
259 public boolean highlightEnabled() {
260 return !isEndingInFlag();
264 public boolean highlight(TerminalInfo ti) {
265 boolean reflexive = isStartTerminal(ti.e, ti.t);
266 if (reflexive && !allowReflexiveConnections())
269 return canConnect(ti.e, ti.t) != null;
273 protected final static Composite ALPHA_COMPOSITE = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.75f);
276 * Root scene graph node for all visualization performed by this tool.
278 protected G2DParentNode ghostNode;
281 * Indicates whether the connection is about to be ended into a new
282 * flag/branchpoint or not.
284 protected TerminalInfo endFlag;
286 protected G2DParentNode endFlagNode;
288 private RouteGraphTarget lastRouteGraphTarget;
291 * @param startTerminal
293 * @param startCanvasPos
295 public ConnectTool2(TerminalInfo startTerminal, int mouseId, Point2D startCanvasPos) {
296 this(startTerminal == null ? Collections.<TerminalInfo> emptyList()
297 : Collections.singletonList(startTerminal),
303 * @param startTerminals
305 * @param startCanvasPos
307 public ConnectTool2(List<TerminalInfo> startTerminals, int mouseId, Point2D startCanvasPos) {
310 if (startCanvasPos == null)
311 throw new NullPointerException("null start position");
312 if (startTerminals == null)
313 throw new NullPointerException("null start terminals");
315 this.startPos = startCanvasPos;
316 this.lastMouseCanvasPos.setLocation(startPos);
318 this.startTerminals = startTerminals;
319 this.startTerminal = startTerminals.isEmpty() ? null : startTerminals.get(0);
323 public void addedToContext(ICanvasContext ctx) {
324 super.addedToContext(ctx);
327 quality.setStaticQuality(Quality.LOW);
329 // Force terminals to always be highlighted without pressing certain
330 // keys or key combinations.
331 originalStrategy = getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY);
332 setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy);
336 protected void onDiagramSet(IDiagram newDiagram, IDiagram oldDiagram) {
337 if (newDiagram != null) {
338 // Get IElementClassProvider
339 ISynchronizationContext ctx = newDiagram.getHint(SynchronizationHints.CONTEXT);
341 this.elementClassProvider = ctx.get(SynchronizationHints.ELEMENT_CLASS_PROVIDER);
344 // See if flags should be created or not.
345 this.createFlags = Boolean.TRUE.equals(newDiagram.getHint(DiagramHints.KEY_USE_CONNECTION_FLAGS));
351 public void removedFromContext(ICanvasContext ctx) {
352 if (getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY) == terminalHoverStrategy) {
353 if (originalStrategy != null)
354 setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, originalStrategy);
356 removeHint(TerminalPainter.TERMINAL_HOVER_STRATEGY);
360 quality.setStaticQuality(null);
362 super.removedFromContext(ctx);
365 protected void startConnection() {
366 Point2D startPos = (Point2D) this.startPos.clone();
367 ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);
368 if (snapAdvisor != null)
369 snapAdvisor.snap(startPos);
371 // Resolve the first element and terminal of the connection.
372 ControlPoint start = new ControlPoint(startPos);
374 if (startTerminal != null) {
375 assert ElementUtils.peekDiagram(startTerminal.e) == diagram;
376 Point2D terminalPos = new Point2D.Double(startTerminal.posDia.getTranslateX(),
377 startTerminal.posDia.getTranslateY());
378 start.setPosition(terminalPos).setAttachedToTerminal(startTerminal);
380 // Create TerminalInfo describing the flag to be created.
382 // This prevents connection creation from creating a branch
383 // point in place of this flag.
384 startFlag = createFlag(EdgeEnd.Begin);
385 start.setAttachedToTerminal(startFlag);
386 showElement(ghostNode, "startFlag", startFlag.e, startPos);
389 controlPoints.add(start);
390 controlPoints.add(new ControlPoint(startPos));
392 // Make sure that we are ending with a flag if ALT is pressed.
393 // This makes the tool always start with a flag which can be quite
394 // cumbersome and is therefore disabled. The current version will not
395 // end the connection if the mouse has not moved at all.
396 //if (keyUtil.isKeyPressed(java.awt.event.KeyEvent.VK_ALT)) {
397 // endWithoutTerminal(lastMouseCanvasPos, true);
402 public void initSG(G2DParentNode parent) {
403 ghostNode = parent.addNode(G2DParentNode.class);
404 ghostNode.setZIndex(PAINT_PRIORITY);
406 ShapeNode pathNode = ghostNode.getOrCreateNode("path", ShapeNode.class);
407 pathNode.setColor(new Color(160, 0, 0));
408 pathNode.setStroke(new BasicStroke(0.1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10,
409 new float[] { 0.5f, 0.2f }, 0));
410 pathNode.setScaleStroke(false);
411 pathNode.setZIndex(0);
413 G2DParentNode points = ghostNode.getOrCreateNode("points", G2DParentNode.class);
419 private RouteTerminal addControlPoint(RouteGraph routeGraph, ControlPoint cp) {
420 TerminalInfo ti = cp.getAttachedTerminal();
421 if(ti != null && ti != startFlag && ti != endFlag) {
422 Rectangle2D bounds = ElementUtils.getElementBoundsOnDiagram(ti.e, new Rectangle2D.Double());
423 GeometryUtils.expandRectangle(bounds, 2);
424 int allowedDirections = RouteGraphConnectionClass.shortestDirectionOutOfBounds(
425 ti.posDia.getTranslateX(), ti.posDia.getTranslateY(), bounds);
426 return routeGraph.addTerminal(ti.posDia.getTranslateX(), ti.posDia.getTranslateY(),
427 bounds, allowedDirections, PlainLineEndStyle.INSTANCE);
430 double x = cp.getPosition().getX();
431 double y = cp.getPosition().getY();
432 int allowedDirections = 0xf;
433 switch(cp.getDirection()) {
434 case Horizontal: allowedDirections = 5; break;
435 case Vertical: allowedDirections = 10; break;
436 case Any: allowedDirections = 15; break;
438 return routeGraph.addTerminal(x, y, x, y, x, y, allowedDirections);
442 protected void updateSG() {
443 if (controlPoints.size() != 2)
446 ControlPoint begin = controlPoints.getFirst();
447 ControlPoint end = controlPoints.getLast();
449 RouteGraph routeGraph = new RouteGraph();
450 RouteTerminal a = addControlPoint(routeGraph, begin);
451 RouteTerminal b = addControlPoint(routeGraph, end);
452 routeGraph.link(a, b);
454 Path2D path = routeGraph.getPath2D();
456 // Create scene graph to visualize the connection.
457 ShapeNode pathNode = ghostNode.getOrCreateNode("path", ShapeNode.class);
458 pathNode.setShape(path);
463 private G2DParentNode showElement(G2DParentNode parent, String nodeId, IElement element, Point2D pos) {
464 return showElement(parent, nodeId, element, AffineTransform.getTranslateInstance(pos.getX(), pos.getY()));
467 private G2DParentNode showElement(G2DParentNode parent, String nodeId, IElement element, AffineTransform tr) {
468 G2DParentNode elementParent = parent.getOrCreateNode(nodeId, G2DParentNode.class);
469 elementParent.setTransform(tr);
470 elementParent.removeNodes();
471 for (SceneGraph sg : element.getElementClass().getItemsByClass(SceneGraph.class))
472 sg.init(element, elementParent);
473 return elementParent;
477 public void cleanupSG() {
482 @EventHandler(priority = 200)
483 public boolean handleCommandEvents(CommandEvent ce) {
484 if (ce.command.equals(Commands.CANCEL)) {
488 } else if (ce.command.equals(Commands.ROTATE_ELEMENT_CCW) || ce.command.equals(Commands.ROTATE_ELEMENT_CW)) {
489 return rotateLastBranchPoint(ce.command.equals(Commands.ROTATE_ELEMENT_CW));
494 @EventHandler(priority = PointerInteractor.TOOL_PRIORITY + 20)
495 public boolean handleKeyEvents(KeyEvent ke) {
496 if (ke instanceof KeyPressedEvent) {
497 // Back-space, cancel prev bend
498 if (ke.keyCode == java.awt.event.KeyEvent.VK_BACK_SPACE)
499 return cancelPreviousBend();
502 if (ke.keyCode == java.awt.event.KeyEvent.VK_ALT) {
504 endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(ke instanceof KeyPressedEvent));
512 @EventHandler(priority = PointerInteractor.TOOL_PRIORITY + 20)
513 public boolean handleEvent(MouseEvent me) {
514 // Only handle events for the connection-initiating mouse
515 if (me.mouseId != mouseId)
518 if (me instanceof MouseMovedEvent)
519 return processMouseMove((MouseMovedEvent) me);
521 if (me instanceof MouseButtonPressedEvent)
522 return processMouseButtonPress((MouseButtonPressedEvent) me);
524 // #7653: Support creating connections between terminals without lifting mouse button in between.
525 if (me instanceof MouseButtonReleasedEvent)
526 return processMouseButtonRelease((MouseButtonReleasedEvent) me);
531 protected boolean processMouseMove(MouseMovedEvent me) {
532 mouseHasMoved = true;
534 Point2D mouseControlPos = me.controlPosition;
535 Point2D mouseCanvasPos = util.controlToCanvas(mouseControlPos, new Point2D.Double());
537 ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);
538 if (snapAdvisor != null)
539 snapAdvisor.snap(mouseCanvasPos);
541 // Record last snapped canvas position of mouse.
542 this.lastMouseCanvasPos.setLocation(mouseCanvasPos);
544 if (isEndingInFlag()) {
545 endFlagNode.setTransform(AffineTransform.getTranslateInstance(mouseCanvasPos.getX(), mouseCanvasPos.getY()));
548 List<TerminalInfo> tis = pi.pickTerminals(me.controlPosition);
549 tis = TerminalUtil.findNearestOverlappingTerminals(tis);
550 if (!tis.isEmpty() && !containsStartTerminal(tis)) {
551 //System.out.println("end terminals (" + tis.size() + "):\n" + EString.implode(tis));
552 for (TerminalInfo ti : tis) {
553 Pair<ConnectionJudgement, TerminalInfo> canConnect = canConnect(ti.e, ti.t);
554 if (canConnect != null) {
555 connectionJudgment = canConnect.first;
557 if (!isEndingInFlag() || !TerminalUtil.isSameTerminal(ti, endTerminal)) {
558 if (canConnect.second != null) {
559 controlPoints.getFirst()
560 .setPosition(canConnect.second.posDia)
561 .setAttachedToTerminal(canConnect.second);
563 controlPoints.getLast()
564 .setPosition(ti.posDia)
565 .setAttachedToTerminal(ti);
567 selectedStartTerminal = canConnect.second;
571 // Make sure that we are ending with a flag if ALT is pressed
572 // and no end terminal is defined.
573 if (!endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me)))
579 RouteGraphTarget cp = RouteGraphConnectTool.pickRouteGraphConnection(
582 pi.getCanvasPickShape(me.controlPosition),
583 pi.getPickDistance());
585 // Remove branch point highlight from previously picked route graph.
586 if (lastRouteGraphTarget != null && cp.getNode() != lastRouteGraphTarget.getNode())
587 cp.getNode().showBranchPoint(null);
588 lastRouteGraphTarget = cp;
590 // Validate connection before visualizing connectability
591 Point2D isectPos = cp.getIntersectionPosition();
592 TerminalInfo ti = TerminalInfo.create(
595 BranchPointTerminal.existingTerminal(
598 BranchPointNode.SHAPE),
599 BranchPointNode.SHAPE);
600 Pair<ConnectionJudgement, TerminalInfo> canConnect = canConnect(ti.e, ti.t);
601 if (canConnect != null) {
602 attachToConnectionJudgement = canConnect.first;
603 controlPoints.getLast().setPosition(ti.posDia).setAttachedToTerminal(ti);
605 startTerminal = canConnect.second;
606 cp.getNode().showBranchPoint(isectPos);
607 if (!endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me)))
612 if (lastRouteGraphTarget != null) {
613 lastRouteGraphTarget.getNode().showBranchPoint(null);
614 lastRouteGraphTarget = null;
619 connectionJudgment = null;
620 attachToConnectionJudgement = null;
621 if (isEndTerminalDefined()) {
622 // CASE: Mouse was previously on top of a valid terminal to end
623 // the connection. Now the mouse has been moved where there is
624 // no longer a terminal to connect to.
626 // => Disconnect the last edge segment from the previous
627 // terminal, mark endElement/endTerminal non-existent
628 // and connect the disconnected edge to a new branch point.
630 controlPoints.getLast()
631 .setPosition(mouseCanvasPos)
632 .setDirection(calculateCurrentBranchPointDirection())
633 .setAttachedToTerminal(null);
637 // CASE: Mouse was not previously on top of a valid ending
640 // => Move and re-orient last branch point.
642 controlPoints.getLast()
643 .setPosition(mouseCanvasPos)
644 .setDirection(calculateCurrentBranchPointDirection());
647 // Make sure that we are ending with a flag if ALT is pressed and no end
648 // terminal is defined.
649 if (!endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me)))
655 protected boolean processMouseButtonPress(MouseButtonPressedEvent e) {
656 MouseButtonEvent me = e;
658 // Do nothing before the mouse has moved at least a little.
659 // This prevents the user from ending the connection right where
664 if (me.button == MouseEvent.LEFT_BUTTON) {
665 Point2D mouseControlPos = me.controlPosition;
666 Point2D mouseCanvasPos = util.getInverseTransform().transform(mouseControlPos, new Point2D.Double());
668 ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);
669 if (snapAdvisor != null)
670 snapAdvisor.snap(mouseCanvasPos);
672 if (tryEndConnection()) {
675 // Finish connection in thin air only if the
676 // connection was started from a valid terminal.
677 if (me.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK) && !startTerminals.isEmpty()) {
678 Pair<ConnectionJudgement, TerminalInfo> pair = canConnect(null, null);
680 connectionJudgment = (ConnectionJudgement) pair.first;
681 selectedStartTerminal = pair.second;
682 // endFlag = createFlag(EdgeEnd.End);
683 // controlPoints.getLast().setAttachedToTerminal(endFlag);
688 // Inform the user why connection couldn't be created.
689 String tmsg = terminalsToString(startTerminals);
690 ErrorLogger.defaultLogWarning("Can't resolve connection type for new connection when starting from one of the following terminals:\n" + tmsg, null);
693 } else if (routePointsAllowed()
694 && (me.stateMask & (MouseEvent.ALT_MASK | MouseEvent.SHIFT_MASK | MouseEvent.CTRL_MASK)) == 0) {
695 // Add new connection control point.
696 controlPoints.add(newControlPointWithCalculatedDirection(mouseCanvasPos));
697 resetForcedBranchPointDirection();
702 // Eat the event to prevent other participants from doing
703 // incompatible things while in this connection mode.
705 } else if (me.button == MouseEvent.RIGHT_BUTTON) {
706 return cancelPreviousBend();
712 private int mouseLeftReleaseCount = 0;
714 protected boolean processMouseButtonRelease(MouseButtonReleasedEvent me) {
715 if (me.button == MouseEvent.LEFT_BUTTON
716 && ++mouseLeftReleaseCount == 1) {
717 return tryEndConnection();
723 * @return <code>true</code> if connection was successfully ended
725 private boolean tryEndConnection() {
726 if (isEndTerminalDefined() && connectionJudgment != null) {
730 } else if (lastRouteGraphTarget != null && attachToConnectionJudgement != null) {
731 lastRouteGraphTarget.getNode().showBranchPoint(null);
732 attachToConnection();
739 private void attachToConnection() {
740 ConnectionJudgement judgment = this.attachToConnectionJudgement;
741 if (judgment == null) {
742 ErrorLogger.defaultLogError("Cannot attach to connection, no judgment available on connection validity", null);
746 ConnectionBuilder builder = new ConnectionBuilder(this.diagram);
747 RouteGraph before = lastRouteGraphTarget.getNode().getRouteGraph();
748 THashMap<Object, Object> copyMap = new THashMap<>();
749 RouteGraph after = before.copy(copyMap);
751 RouteLine attachTo = (RouteLine) copyMap.get(lastRouteGraphTarget.getLine());
752 after.makePersistent(attachTo);
753 for (RouteLine line : after.getAllLines()) {
754 if (!line.isTransient() && line.isHorizontal() == attachTo.isHorizontal()
755 && line.getPosition() == attachTo.getPosition()) {
760 RouteLine attachToLine = attachTo;
761 RouteGraphDelta delta = new RouteGraphDelta(before, after);
763 Simantics.getSession().asyncRequest(new WriteRequest() {
765 public void perform(WriteGraph graph) throws DatabaseException {
766 graph.markUndoPoint();
767 Resource connection = ElementUtils.getObject(endTerminal.e);
768 if (!delta.isEmpty()) {
769 new RouteGraphConnection(graph, connection).synchronize(graph, before, after, delta);
771 Resource line = RouteGraphConnection.deserialize(graph, attachToLine.getData());
772 Deque<ControlPoint> cps = new ArrayDeque<>();
773 for (Iterator<ControlPoint> iterator = controlPoints.descendingIterator(); iterator.hasNext();)
774 cps.add(iterator.next());
775 builder.attachToRouteGraph(graph, judgment, connection, line, cps, startTerminal, FlagClass.Type.In);
778 if (parameter != null)
779 ExceptionUtils.logAndShowError(parameter);
783 protected boolean cancelPreviousBend() {
784 if (!routePointsAllowed())
787 // Just to make this code more comprehensible, prevent an editing
788 // case that requires ugly code to work.
789 if (isEndingInFlag())
792 // If there are no real route points, cancel whole connection.
793 if (controlPoints.size() <= 2) {
800 controlPoints.removeLast();
801 controlPoints.getLast().setPosition(lastMouseCanvasPos);
802 resetForcedBranchPointDirection();
809 * Rotates the last branch point in the created connection in either
810 * clockwise or counter-clockwise direction as a response to a user
814 * At the same time it use {@link #forcedBranchPointDirection} to mark the
815 * current last branch point to be forcefully oriented according to the
816 * users wishes instead of calculating a default value for the orientation
817 * from the routed connection path. See
818 * {@link #calculateCurrentBranchPointDirection()} for more information on
822 * The logic of this method goes as follows:
824 * <li>Calculate the current branch point direction</li>
825 * <li>If the branch point direction is currently user selected (
826 * {@link #forcedBranchPointDirection}</li>
832 * @return <code>true</code> if the rotation was successful
834 protected boolean rotateLastBranchPoint(boolean clockwise) {
835 Direction oldDir = calculateCurrentBranchPointDirection();
837 if (forcedBranchPointDirection == null) {
838 forcedBranchPointDirection = oldDir.toggleDetermined();
840 forcedBranchPointDirection = clockwise ? oldDir.cycleNext() : oldDir.cyclePrevious();
843 controlPoints.getLast().setDirection(forcedBranchPointDirection);
851 * Set preferred direction for a branch/route point element.
853 * @param branchPoint the element to set the direction for
854 * @param direction the direction to set
857 protected void setDirection(IElement branchPoint, Direction direction) {
858 branchPoint.getElementClass().getSingleItem(BranchPoint.class).setDirectionPreference(branchPoint, direction);
861 protected Direction forcedBranchPointDirection() {
862 return forcedBranchPointDirection;
865 protected void resetForcedBranchPointDirection() {
866 forcedBranchPointDirection = null;
869 protected void forceBranchPointDirection(Direction direction) {
870 forcedBranchPointDirection = direction;
876 protected Direction calculateCurrentBranchPointDirection() {
877 // If this is not the first branch point, toggle direction compared to
879 if (forcedBranchPointDirection != null)
880 return forcedBranchPointDirection;
882 if (controlPoints.size() > 2) {
883 // This is not the first edge segment, toggle route point
885 Iterator<ControlPoint> it = controlPoints.descendingIterator();
887 ControlPoint secondLastCp = it.next();
889 Direction dir = secondLastCp.getDirection();
892 return Direction.Vertical;
894 return Direction.Horizontal;
899 // If this is the first branch point, calculate based on edge segment
901 if (controlPoints.size() > 1) {
902 Iterator<ControlPoint> it = controlPoints.descendingIterator();
903 ControlPoint last = it.next();
904 ControlPoint secondLast = it.next();
906 double angle = Math.atan2(Math.abs(last.getPosition().getY() - secondLast.getPosition().getY()),
907 Math.abs(last.getPosition().getX() - secondLast.getPosition().getX()));
909 if (angle >= 0 && angle < Math.PI / 4) {
910 return Direction.Horizontal;
911 } else if (angle > Math.PI / 4 && angle <= Math.PI / 2) {
912 return Direction.Vertical;
916 return Direction.Any;
919 protected boolean isEndingInFlag() {
920 return endFlag != null;
926 * @return <code>true</code> if updateSG was executed, <code>false</code>
929 protected boolean endWithoutTerminal(Point2D mousePos, boolean altDown) {
930 // Just go with branch points if flags are not allowed.
934 boolean endTerminalDefined = isEndTerminalDefined();
937 if (!isEndingInFlag()) {
938 endFlag = createFlag(EdgeEnd.End);
939 endFlagNode = showElement(ghostNode, "endFlag", endFlag.e, mousePos);
940 controlPoints.getLast()
941 .setDirection(calculateCurrentBranchPointDirection())
942 .setAttachedToTerminal(endFlag);
944 // TerminalPainter must refresh
945 setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy);
951 if (isEndingInFlag()) {
952 // Currently ending with flag but ALT is no longer down
953 // so that flag must be removed.
955 endFlagNode.remove();
958 ControlPoint cp = controlPoints.getLast();
959 cp.setDirection(calculateCurrentBranchPointDirection())
960 .setAttachedToTerminal(endTerminal);
962 if (endTerminalDefined) {
963 cp.setPosition(endTerminal.posDia);
965 cp.setPosition(mousePos);
968 // Force TerminalPainter refresh
969 setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy);
978 protected void createConnection() {
980 this.selectedStartTerminal,
982 this.connectionJudgment,
986 protected void createConnection(
987 final TerminalInfo startTerminal,
988 final TerminalInfo endTerminal,
989 final ConnectionJudgement judgement,
990 final Deque<ControlPoint> controlPoints)
992 TimeLogger.resetTimeAndLog(getClass(), "createConnection");
993 if (judgement == null) {
994 // Inform the user why connection couldn't be created.
995 String tmsg = terminalsToString(Arrays.asList(startTerminal, endTerminal));
996 ErrorLogger.defaultLogError("Cannot create connection, no judgment available on connection validity when connecting the terminals:\n" + tmsg, null);
1000 final ConnectionBuilder builder = new ConnectionBuilder(this.diagram);
1002 Simantics.getSession().asyncRequest(new WriteRequest() {
1004 public void perform(WriteGraph graph) throws DatabaseException {
1005 builder.create(graph, judgement, controlPoints, startTerminal, endTerminal);
1008 if (parameter != null)
1009 ExceptionUtils.logAndShowError(parameter);
1017 protected ControlPoint newControlPointWithCalculatedDirection(Point2D canvasPos) {
1018 return new ControlPoint(canvasPos, calculateCurrentBranchPointDirection());
1024 * @return <code>true</code> if the specified element terminal matches any
1025 * TerminalInfo in {@link #startTerminals}
1027 protected boolean isStartTerminal(IElement e, Terminal t) {
1028 if (startTerminal == null)
1030 for (TerminalInfo st : startTerminals) {
1031 if (st.e == e && st.t == t) {
1041 * @return <code>true</code> if the specified element terminal matches any
1042 * TerminalInfo in {@link #startTerminals}
1044 protected boolean containsStartTerminal(List<TerminalInfo> tis) {
1045 if (startTerminal == null)
1047 for (TerminalInfo st : startTerminals) {
1048 for (TerminalInfo et : tis) {
1049 if (st.e == et.e && st.t == et.t) {
1057 protected static FlagClass.Type endToFlagType(EdgeEnd end) {
1060 return FlagClass.Type.In;
1062 return FlagClass.Type.Out;
1064 throw new IllegalArgumentException("unrecognized edge end: " + end);
1068 protected TerminalInfo createFlag(EdgeEnd connectionEnd) {
1069 ElementClass flagClass = elementClassProvider.get(ElementClasses.FLAG);
1070 IElement e = Element.spawnNew(flagClass);
1072 e.setHint(FlagClass.KEY_FLAG_TYPE, endToFlagType(connectionEnd));
1073 e.setHint(FlagClass.KEY_FLAG_MODE, FlagClass.Mode.Internal);
1075 TerminalInfo ti = new TerminalInfo();
1077 ti.t = ElementUtils.getSingleTerminal(e);
1078 ti.posElem = TerminalUtil.getTerminalPosOnElement(e, ti.t);
1079 ti.posDia = TerminalUtil.getTerminalPosOnDiagram(e, ti.t);
1084 protected boolean shouldEndWithFlag(MouseEvent me) {
1085 return shouldEndWithFlag( me.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK) );
1088 protected boolean shouldEndWithFlag(boolean altPressed) {
1089 return altPressed && !isEndTerminalDefined() && createFlags && startFlag == null;
1092 protected boolean isEndTerminalDefined() {
1093 return endTerminal != null;
1096 protected boolean isFlagTerminal(TerminalInfo ti) {
1097 return ti.e.getElementClass().containsClass(FlagHandler.class);
1100 protected boolean allowReflexiveConnections() {
1104 protected boolean routePointsAllowed() {
1105 return Boolean.TRUE.equals(diagram.getHint(DiagramHints.KEY_ALLOW_ROUTE_POINTS));
1110 * @param endTerminal
1113 @SuppressWarnings("unchecked")
1114 protected final Pair<ConnectionJudgement, TerminalInfo> canConnect(IElement endElement, Terminal endTerminal) {
1115 IConnectionAdvisor advisor = diagram.getHint(DiagramHints.CONNECTION_ADVISOR);
1116 Object judgement = canConnect(advisor, endElement, endTerminal);
1117 if (judgement == null)
1119 if (judgement instanceof Pair<?, ?>)
1120 return (Pair<ConnectionJudgement, TerminalInfo>) judgement;
1121 return Pair.<ConnectionJudgement, TerminalInfo>make((ConnectionJudgement) judgement, startTerminal);
1124 protected Object canConnect(IConnectionAdvisor advisor, IElement endElement, Terminal endTerminal) {
1125 if (advisor == null)
1126 return Pair.make(ConnectionJudgement.CANBEMADELEGAL, startTerminal);
1127 if (startTerminals.isEmpty()) {
1128 ConnectionJudgement obj = (ConnectionJudgement) advisor.canBeConnected(null, null, null, endElement, endTerminal);
1129 return obj != null ? Pair.<ConnectionJudgement, TerminalInfo>make(obj, null) : null;
1131 for (TerminalInfo st : startTerminals) {
1132 ConnectionJudgement obj = (ConnectionJudgement) advisor.canBeConnected(null, st.e, st.t, endElement, endTerminal);
1134 return Pair.make(obj, st);
1141 * For generating debugging information of what was attempted by the user
1142 * when a connection couldn't be created.
1147 private String terminalsToString(final Iterable<TerminalInfo> ts) {
1149 return Simantics.sync(new UniqueRead<String>() {
1151 public String perform(ReadGraph graph) throws DatabaseException {
1152 DiagramResource DIA = DiagramResource.getInstance(graph);
1153 ModelingResources MOD = ModelingResources.getInstance(graph);
1154 StringBuilder sb = new StringBuilder();
1155 boolean first = true;
1156 for (TerminalInfo ti : ts) {
1160 sb.append("element ");
1161 Object o = ElementUtils.getObject(ti.e);
1162 if (o instanceof Resource) {
1163 Resource er = (Resource) o;
1164 Resource cer = graph.getPossibleObject(er, MOD.ElementToComponent);
1165 Resource r = cer != null ? cer : er;
1166 sb.append(NameUtils.getSafeName(graph, r)).append(" : ");
1167 for (Resource type : graph.getPrincipalTypes(r)) {
1168 sb.append(NameUtils.getSafeName(graph, type, true));
1171 sb.append(ti.e.toString());
1173 sb.append(", terminal ");
1174 if (ti.t instanceof ResourceTerminal) {
1175 Resource tr = ((ResourceTerminal) ti.t).getResource();
1176 Resource cp = graph.getPossibleObject(tr, DIA.HasConnectionPoint);
1177 Resource r = cp != null ? cp : tr;
1178 sb.append(NameUtils.getSafeName(graph, r, true));
1180 sb.append(ti.t.toString());
1183 return sb.toString();
1186 } catch (DatabaseException e) {
1187 return e.getMessage();