1 /*******************************************************************************
\r
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
\r
3 * in Industry THTH ry.
\r
4 * All rights reserved. This program and the accompanying materials
\r
5 * are made available under the terms of the Eclipse Public License v1.0
\r
6 * which accompanies this distribution, and is available at
\r
7 * http://www.eclipse.org/legal/epl-v10.html
\r
10 * VTT Technical Research Centre of Finland - initial API and implementation
\r
11 *******************************************************************************/
\r
12 package org.simantics.diagram.participant;
\r
14 import java.awt.AlphaComposite;
\r
15 import java.awt.BasicStroke;
\r
16 import java.awt.Color;
\r
17 import java.awt.Composite;
\r
18 import java.awt.geom.AffineTransform;
\r
19 import java.awt.geom.Path2D;
\r
20 import java.awt.geom.Point2D;
\r
21 import java.awt.geom.Rectangle2D;
\r
22 import java.util.ArrayDeque;
\r
23 import java.util.ArrayList;
\r
24 import java.util.Arrays;
\r
25 import java.util.Collection;
\r
26 import java.util.Collections;
\r
27 import java.util.Deque;
\r
28 import java.util.Iterator;
\r
29 import java.util.List;
\r
31 import org.simantics.Simantics;
\r
32 import org.simantics.db.ReadGraph;
\r
33 import org.simantics.db.Resource;
\r
34 import org.simantics.db.WriteGraph;
\r
35 import org.simantics.db.common.request.UniqueRead;
\r
36 import org.simantics.db.common.request.WriteRequest;
\r
37 import org.simantics.db.common.utils.NameUtils;
\r
38 import org.simantics.db.exception.DatabaseException;
\r
39 import org.simantics.diagram.connection.RouteGraph;
\r
40 import org.simantics.diagram.connection.RouteGraphConnectionClass;
\r
41 import org.simantics.diagram.connection.RouteLine;
\r
42 import org.simantics.diagram.connection.RouteTerminal;
\r
43 import org.simantics.diagram.connection.delta.RouteGraphDelta;
\r
44 import org.simantics.diagram.connection.rendering.arrows.PlainLineEndStyle;
\r
45 import org.simantics.diagram.content.ResourceTerminal;
\r
46 import org.simantics.diagram.stubs.DiagramResource;
\r
47 import org.simantics.diagram.synchronization.ISynchronizationContext;
\r
48 import org.simantics.diagram.synchronization.SynchronizationHints;
\r
49 import org.simantics.diagram.synchronization.graph.RouteGraphConnection;
\r
50 import org.simantics.g2d.canvas.ICanvasContext;
\r
51 import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;
\r
52 import org.simantics.g2d.canvas.impl.DependencyReflection.Reference;
\r
53 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;
\r
54 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit;
\r
55 import org.simantics.g2d.connection.IConnectionAdvisor;
\r
56 import org.simantics.g2d.diagram.DiagramHints;
\r
57 import org.simantics.g2d.diagram.DiagramUtils;
\r
58 import org.simantics.g2d.diagram.IDiagram;
\r
59 import org.simantics.g2d.diagram.handler.PickContext;
\r
60 import org.simantics.g2d.diagram.handler.Topology.Terminal;
\r
61 import org.simantics.g2d.diagram.participant.ElementPainter;
\r
62 import org.simantics.g2d.diagram.participant.TerminalPainter;
\r
63 import org.simantics.g2d.diagram.participant.TerminalPainter.TerminalHoverStrategy;
\r
64 import org.simantics.g2d.diagram.participant.pointertool.AbstractMode;
\r
65 import org.simantics.g2d.diagram.participant.pointertool.PointerInteractor;
\r
66 import org.simantics.g2d.diagram.participant.pointertool.TerminalUtil;
\r
67 import org.simantics.g2d.diagram.participant.pointertool.TerminalUtil.TerminalInfo;
\r
68 import org.simantics.g2d.element.ElementClass;
\r
69 import org.simantics.g2d.element.ElementClasses;
\r
70 import org.simantics.g2d.element.ElementUtils;
\r
71 import org.simantics.g2d.element.IElement;
\r
72 import org.simantics.g2d.element.IElementClassProvider;
\r
73 import org.simantics.g2d.element.handler.EdgeVisuals.EdgeEnd;
\r
74 import org.simantics.g2d.element.handler.SceneGraph;
\r
75 import org.simantics.g2d.element.handler.TerminalTopology;
\r
76 import org.simantics.g2d.element.handler.impl.BranchPointTerminal;
\r
77 import org.simantics.g2d.element.impl.Element;
\r
78 import org.simantics.g2d.elementclass.BranchPoint;
\r
79 import org.simantics.g2d.elementclass.BranchPoint.Direction;
\r
80 import org.simantics.g2d.elementclass.FlagClass;
\r
81 import org.simantics.g2d.elementclass.FlagHandler;
\r
82 import org.simantics.g2d.participant.RenderingQualityInteractor;
\r
83 import org.simantics.g2d.participant.TransformUtil;
\r
84 import org.simantics.g2d.utils.geom.DirectionSet;
\r
85 import org.simantics.modeling.ModelingResources;
\r
86 import org.simantics.scenegraph.g2d.G2DParentNode;
\r
87 import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler;
\r
88 import org.simantics.scenegraph.g2d.events.KeyEvent;
\r
89 import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
\r
90 import org.simantics.scenegraph.g2d.events.MouseEvent;
\r
91 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonEvent;
\r
92 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
\r
93 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
\r
94 import org.simantics.scenegraph.g2d.events.command.CommandEvent;
\r
95 import org.simantics.scenegraph.g2d.events.command.Commands;
\r
96 import org.simantics.scenegraph.g2d.nodes.BranchPointNode;
\r
97 import org.simantics.scenegraph.g2d.nodes.ShapeNode;
\r
98 import org.simantics.scenegraph.g2d.snap.ISnapAdvisor;
\r
99 import org.simantics.scenegraph.utils.GeometryUtils;
\r
100 import org.simantics.scenegraph.utils.Quality;
\r
101 import org.simantics.structural2.modelingRules.ConnectionJudgement;
\r
102 import org.simantics.utils.datastructures.Callback;
\r
103 import org.simantics.utils.datastructures.Pair;
\r
104 import org.simantics.utils.logging.TimeLogger;
\r
105 import org.simantics.utils.ui.ErrorLogger;
\r
106 import org.simantics.utils.ui.ExceptionUtils;
\r
108 import gnu.trove.map.hash.THashMap;
\r
111 * A basic tool for making connection on diagrams.
\r
113 * This version defines the starting, ending and route points of a connection.
\r
114 * The routing itself is left up to the diagram router employed by
\r
115 * {@link DiagramUtils#validateAndFix(IDiagram, ICanvasContext)}.
\r
119 * This tool is added to the diagram when a connection sequence is initiated by
\r
120 * another participant. PointerInteractor is one such participant which adds the
\r
121 * tool when a terminal or non-terminal-occupied canvas space is ALT+clicked
\r
122 * (see {@link PointerInteractor#checkInitiateConnectTool(MouseEvent, Point2D)}
\r
123 * ). The connection will be finished when another allowed terminal is clicked
\r
124 * upon or empty canvas space is ALT+clicked. Route points for the connection
\r
125 * can be created by clicking around on non-terminal-occupied canvas space while
\r
129 * Connections can be started from and ended in flags by pressing ALT while
\r
132 * @author Tuukka Lehtonen
\r
134 public class ConnectTool2 extends AbstractMode {
\r
136 public static final int PAINT_PRIORITY = ElementPainter.ELEMENT_PAINT_PRIORITY + 5;
\r
139 protected RenderingQualityInteractor quality;
\r
142 protected TransformUtil util;
\r
145 protected ElementPainter diagramPainter;
\r
148 protected PointerInteractor pi;
\r
151 protected PickContext pickContext;
\r
154 * Start element terminal of the connection. <code>null</code> if connection
\r
155 * was started from a flag or a branch point.
\r
157 * The value is received by the constructor.
\r
159 protected List<TerminalInfo> startTerminals;
\r
162 * Refers to any of the possible overlapping start terminals. The value is
\r
163 * taken from the first index of {@link #startTerminals} assuming that the
\r
164 * first one is the nearest. It is <code>null</code> if
\r
165 * {@link #startTerminals} is empty.
\r
167 protected TerminalInfo startTerminal;
\r
169 protected TerminalInfo startFlag;
\r
172 * Starting position of the connection, received as an external argument.
\r
174 protected final Point2D startPos;
\r
177 * <code>true</code> if this tool should create connection continuation
\r
178 * flags, <code>false</code> otherwise.
\r
180 protected boolean createFlags;
\r
185 protected IElementClassProvider elementClassProvider;
\r
190 protected Deque<ControlPoint> controlPoints = new ArrayDeque<ControlPoint>();
\r
193 * Contains <code>null</code> when a connection is started from a new flag
\r
194 * or one of the terminals in {@link #startTerminals} when a connection is
\r
195 * being created starting from a terminal or possibly a set of terminals.
\r
198 * Note that this is different from {@link #startTerminal} which simply
\r
199 * represents the first element of {@link #startTerminals}.
\r
202 * Only when this value and {@link #endTerminal} is properly set will a
\r
203 * connection be created between two element terminals.
\r
205 protected TerminalInfo selectedStartTerminal;
\r
208 * Element terminal of connection end element. <code>null</code> if
\r
209 * connection cannot be ended where it is currently being attempted to end.
\r
211 protected TerminalInfo endTerminal;
\r
214 * The latest connectability judgment from the active
\r
215 * {@link IConnectionAdvisor} should the connection happen between
\r
216 * {@link #selectedStartTerminal} and {@link #endTerminal}.
\r
218 protected ConnectionJudgement connectionJudgment;
\r
221 * If non-null during connection drawing this field tells the direction
\r
222 * forced for the current branch point by the user through the UI commands
\r
223 * {@link Commands#ROTATE_ELEMENT_CCW} and
\r
224 * {@link Commands#ROTATE_ELEMENT_CW}.
\r
226 private Direction forcedBranchPointDirection;
\r
229 * A temporary variable for use with
\r
230 * {@link TerminalTopology#getTerminals(IElement, Collection)}.
\r
232 protected Collection<Terminal> terminals = new ArrayList<Terminal>();
\r
235 * Previous mouse canvas position recorded by
\r
236 * {@link #processMouseMove(MouseMovedEvent)}.
\r
238 protected Point2D lastMouseCanvasPos = new Point2D.Double();
\r
241 * Set to true once {@link #processMouseMove(MouseMovedEvent)} has been
\r
242 * invoked at least once. This is used to tell whether to allow creation of
\r
243 * branch points or finising the connection in thin air. It will not be
\r
244 * allowed if the mouse has not moved at all since starting the connection.
\r
246 protected boolean mouseHasMoved = false;
\r
248 protected TerminalHoverStrategy originalStrategy = null;
\r
250 protected TerminalHoverStrategy terminalHoverStrategy = new TerminalHoverStrategy() {
\r
252 public boolean highlightEnabled() {
\r
253 return !isEndingInFlag();
\r
257 public boolean highlight(TerminalInfo ti) {
\r
258 boolean reflexive = isStartTerminal(ti.e, ti.t);
\r
259 if (reflexive && !allowReflexiveConnections())
\r
262 return canConnect(ti.e, ti.t) != null;
\r
266 protected final static Composite ALPHA_COMPOSITE = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.75f);
\r
269 * Root scene graph node for all visualization performed by this tool.
\r
271 protected G2DParentNode ghostNode;
\r
274 * Indicates whether the connection is about to be ended into a new
\r
275 * flag/branchpoint or not.
\r
277 protected TerminalInfo endFlag;
\r
279 protected G2DParentNode endFlagNode;
\r
281 private RouteGraphTarget lastRouteGraphTarget;
\r
284 * @param startTerminal
\r
286 * @param startCanvasPos
\r
288 public ConnectTool2(TerminalInfo startTerminal, int mouseId, Point2D startCanvasPos) {
\r
289 this(startTerminal == null ? Collections.<TerminalInfo> emptyList()
\r
290 : Collections.singletonList(startTerminal),
\r
296 * @param startTerminals
\r
298 * @param startCanvasPos
\r
300 public ConnectTool2(List<TerminalInfo> startTerminals, int mouseId, Point2D startCanvasPos) {
\r
303 if (startCanvasPos == null)
\r
304 throw new NullPointerException("null start position");
\r
305 if (startTerminals == null)
\r
306 throw new NullPointerException("null start terminals");
\r
308 this.startPos = startCanvasPos;
\r
309 this.lastMouseCanvasPos.setLocation(startPos);
\r
311 this.startTerminals = startTerminals;
\r
312 this.startTerminal = startTerminals.isEmpty() ? null : startTerminals.get(0);
\r
316 public void addedToContext(ICanvasContext ctx) {
\r
317 super.addedToContext(ctx);
\r
319 if (quality != null)
\r
320 quality.setStaticQuality(Quality.LOW);
\r
322 // Force terminals to always be highlighted without pressing certain
\r
323 // keys or key combinations.
\r
324 originalStrategy = getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY);
\r
325 setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy);
\r
329 protected void onDiagramSet(IDiagram newDiagram, IDiagram oldDiagram) {
\r
330 if (newDiagram != null) {
\r
331 // Get IElementClassProvider
\r
332 ISynchronizationContext ctx = newDiagram.getHint(SynchronizationHints.CONTEXT);
\r
334 this.elementClassProvider = ctx.get(SynchronizationHints.ELEMENT_CLASS_PROVIDER);
\r
337 // See if flags should be created or not.
\r
338 this.createFlags = Boolean.TRUE.equals(newDiagram.getHint(DiagramHints.KEY_USE_CONNECTION_FLAGS));
\r
344 public void removedFromContext(ICanvasContext ctx) {
\r
345 if (getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY) == terminalHoverStrategy) {
\r
346 if (originalStrategy != null)
\r
347 setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, originalStrategy);
\r
349 removeHint(TerminalPainter.TERMINAL_HOVER_STRATEGY);
\r
352 if (quality != null)
\r
353 quality.setStaticQuality(null);
\r
355 super.removedFromContext(ctx);
\r
358 protected void startConnection() {
\r
359 Point2D startPos = (Point2D) this.startPos.clone();
\r
360 ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);
\r
361 if (snapAdvisor != null)
\r
362 snapAdvisor.snap(startPos);
\r
364 // Resolve the first element and terminal of the connection.
\r
365 ControlPoint start = new ControlPoint(startPos);
\r
367 if (startTerminal != null) {
\r
368 assert ElementUtils.peekDiagram(startTerminal.e) == diagram;
\r
369 Point2D terminalPos = new Point2D.Double(startTerminal.posDia.getTranslateX(),
\r
370 startTerminal.posDia.getTranslateY());
\r
371 start.setPosition(terminalPos).setAttachedToTerminal(startTerminal);
\r
373 // Create TerminalInfo describing the flag to be created.
\r
375 // This prevents connection creation from creating a branch
\r
376 // point in place of this flag.
\r
377 startFlag = createFlag(EdgeEnd.Begin);
\r
378 start.setAttachedToTerminal(startFlag);
\r
379 showElement(ghostNode, "startFlag", startFlag.e, startPos);
\r
382 controlPoints.add(start);
\r
383 controlPoints.add(new ControlPoint(startPos));
\r
385 // Make sure that we are ending with a flag if ALT is pressed.
\r
386 // This makes the tool always start with a flag which can be quite
\r
387 // cumbersome and is therefore disabled. The current version will not
\r
388 // end the connection if the mouse has not moved at all.
\r
389 //if (keyUtil.isKeyPressed(java.awt.event.KeyEvent.VK_ALT)) {
\r
390 // endWithoutTerminal(lastMouseCanvasPos, true);
\r
395 public void initSG(G2DParentNode parent) {
\r
396 ghostNode = parent.addNode(G2DParentNode.class);
\r
397 ghostNode.setZIndex(PAINT_PRIORITY);
\r
399 ShapeNode pathNode = ghostNode.getOrCreateNode("path", ShapeNode.class);
\r
400 pathNode.setColor(new Color(160, 0, 0));
\r
401 pathNode.setStroke(new BasicStroke(0.1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10,
\r
402 new float[] { 0.5f, 0.2f }, 0));
\r
403 pathNode.setScaleStroke(false);
\r
404 pathNode.setZIndex(0);
\r
406 G2DParentNode points = ghostNode.getOrCreateNode("points", G2DParentNode.class);
\r
407 points.setZIndex(1);
\r
412 static class Segment {
\r
413 public final ControlPoint begin;
\r
414 public final ControlPoint end;
\r
415 public Path2D path;
\r
417 public Segment(ControlPoint begin, ControlPoint end) {
\r
418 this.begin = begin;
\r
423 public String toString() {
\r
424 return "Segment[begin=" + begin + ", end=" + end + ", path=" + path + "]";
\r
428 private RouteTerminal addControlPoint(RouteGraph routeGraph, ControlPoint cp) {
\r
429 TerminalInfo ti = cp.getAttachedTerminal();
\r
430 if(ti != null && ti != startFlag && ti != endFlag) {
\r
431 Rectangle2D bounds = ElementUtils.getElementBoundsOnDiagram(ti.e, new Rectangle2D.Double());
\r
432 GeometryUtils.expandRectangle(bounds, 2);
\r
433 int allowedDirections = RouteGraphConnectionClass.shortestDirectionOutOfBounds(
\r
434 ti.posDia.getTranslateX(), ti.posDia.getTranslateY(), bounds);
\r
435 return routeGraph.addTerminal(ti.posDia.getTranslateX(), ti.posDia.getTranslateY(),
\r
436 bounds, allowedDirections, PlainLineEndStyle.INSTANCE);
\r
439 double x = cp.getPosition().getX();
\r
440 double y = cp.getPosition().getY();
\r
441 int allowedDirections = 0xf;
\r
442 switch(cp.getDirection()) {
\r
443 case Horizontal: allowedDirections = 5; break;
\r
444 case Vertical: allowedDirections = 10; break;
\r
445 case Any: allowedDirections = 15; break;
\r
447 return routeGraph.addTerminal(x, y, x, y, x, y, allowedDirections);
\r
451 protected void updateSG() {
\r
452 if (controlPoints.size() != 2)
\r
455 ControlPoint begin = controlPoints.getFirst();
\r
456 ControlPoint end = controlPoints.getLast();
\r
458 RouteGraph routeGraph = new RouteGraph();
\r
459 RouteTerminal a = addControlPoint(routeGraph, begin);
\r
460 RouteTerminal b = addControlPoint(routeGraph, end);
\r
461 routeGraph.link(a, b);
\r
463 Path2D path = routeGraph.getPath2D();
\r
465 // Create scene graph to visualize the connection.
\r
466 ShapeNode pathNode = ghostNode.getOrCreateNode("path", ShapeNode.class);
\r
467 pathNode.setShape(path);
\r
472 private G2DParentNode showElement(G2DParentNode parent, String nodeId, IElement element, Point2D pos) {
\r
473 return showElement(parent, nodeId, element, AffineTransform.getTranslateInstance(pos.getX(), pos.getY()));
\r
476 private G2DParentNode showElement(G2DParentNode parent, String nodeId, IElement element, AffineTransform tr) {
\r
477 G2DParentNode elementParent = parent.getOrCreateNode(nodeId, G2DParentNode.class);
\r
478 elementParent.setTransform(tr);
\r
479 elementParent.removeNodes();
\r
480 for (SceneGraph sg : element.getElementClass().getItemsByClass(SceneGraph.class))
\r
481 sg.init(element, elementParent);
\r
482 return elementParent;
\r
485 private List<Segment> toSegments(Deque<ControlPoint> points) {
\r
486 if (points.isEmpty())
\r
487 return Collections.emptyList();
\r
489 List<Segment> segments = new ArrayList<Segment>();
\r
491 Iterator<ControlPoint> it = points.iterator();
\r
492 ControlPoint prev = it.next();
\r
493 while (it.hasNext()) {
\r
494 ControlPoint next = it.next();
\r
495 segments.add(new Segment(prev, next));
\r
503 public void cleanupSG() {
\r
504 ghostNode.remove();
\r
508 @EventHandler(priority = 200)
\r
509 public boolean handleCommandEvents(CommandEvent ce) {
\r
510 if (ce.command.equals(Commands.CANCEL)) {
\r
514 } else if (ce.command.equals(Commands.ROTATE_ELEMENT_CCW) || ce.command.equals(Commands.ROTATE_ELEMENT_CW)) {
\r
515 return rotateLastBranchPoint(ce.command.equals(Commands.ROTATE_ELEMENT_CW));
\r
520 @EventHandler(priority = PointerInteractor.TOOL_PRIORITY + 20)
\r
521 public boolean handleKeyEvents(KeyEvent ke) {
\r
522 if (ke instanceof KeyPressedEvent) {
\r
523 // Back-space, cancel prev bend
\r
524 if (ke.keyCode == java.awt.event.KeyEvent.VK_BACK_SPACE)
\r
525 return cancelPreviousBend();
\r
528 if (ke.keyCode == java.awt.event.KeyEvent.VK_ALT) {
\r
530 endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(ke instanceof KeyPressedEvent));
\r
538 @EventHandler(priority = PointerInteractor.TOOL_PRIORITY + 20)
\r
539 public boolean handleEvent(MouseEvent me) {
\r
540 // Only handle events for the connection-initiating mouse
\r
541 if (me.mouseId != mouseId)
\r
544 if (me instanceof MouseMovedEvent)
\r
545 return processMouseMove((MouseMovedEvent) me);
\r
547 if (me instanceof MouseButtonPressedEvent)
\r
548 return processMouseButtonPress((MouseButtonPressedEvent) me);
\r
553 protected boolean processMouseMove(MouseMovedEvent me) {
\r
554 mouseHasMoved = true;
\r
556 Point2D mouseControlPos = me.controlPosition;
\r
557 Point2D mouseCanvasPos = util.controlToCanvas(mouseControlPos, new Point2D.Double());
\r
559 ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);
\r
560 if (snapAdvisor != null)
\r
561 snapAdvisor.snap(mouseCanvasPos);
\r
563 // Record last snapped canvas position of mouse.
\r
564 this.lastMouseCanvasPos.setLocation(mouseCanvasPos);
\r
566 if (isEndingInFlag()) {
\r
567 endFlagNode.setTransform(AffineTransform.getTranslateInstance(mouseCanvasPos.getX(), mouseCanvasPos.getY()));
\r
570 List<TerminalInfo> tis = pi.pickTerminals(me.controlPosition);
\r
571 tis = TerminalUtil.findNearestOverlappingTerminals(tis);
\r
572 if (!tis.isEmpty() && !containsStartTerminal(tis)) {
\r
573 //System.out.println("end terminals (" + tis.size() + "):\n" + EString.implode(tis));
\r
574 for (TerminalInfo ti : tis) {
\r
575 Pair<ConnectionJudgement, TerminalInfo> canConnect = canConnect(ti.e, ti.t);
\r
576 if (canConnect != null) {
\r
577 connectionJudgment = canConnect.first;
\r
579 if (!isEndingInFlag() || !TerminalUtil.isSameTerminal(ti, endTerminal)) {
\r
580 if (canConnect.second != null) {
\r
581 controlPoints.getFirst()
\r
582 .setPosition(canConnect.second.posDia)
\r
583 .setAttachedToTerminal(canConnect.second);
\r
585 controlPoints.getLast()
\r
586 .setPosition(ti.posDia)
\r
587 .setAttachedToTerminal(ti);
\r
589 selectedStartTerminal = canConnect.second;
\r
593 // Make sure that we are ending with a flag if ALT is pressed
\r
594 // and no end terminal is defined.
\r
595 if (!endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me)))
\r
601 RouteGraphTarget cp = RouteGraphConnectTool.pickRouteGraphConnection(
\r
603 pi.getCanvasPickShape(me.controlPosition),
\r
604 pi.getPickDistance());
\r
606 // Remove branch point highlight from previously picked route graph.
\r
607 if (lastRouteGraphTarget != null && cp.getNode() != lastRouteGraphTarget.getNode())
\r
608 cp.getNode().showBranchPoint(null);
\r
609 lastRouteGraphTarget = cp;
\r
611 // Validate connection before visualizing connectability
\r
612 Point2D isectPos = cp.getIntersectionPosition();
\r
613 TerminalInfo ti = TerminalInfo.create(
\r
616 BranchPointTerminal.existingTerminal(
\r
619 BranchPointNode.SHAPE),
\r
620 BranchPointNode.SHAPE);
\r
621 Pair<ConnectionJudgement, TerminalInfo> canConnect = canConnect(ti.e, ti.t);
\r
622 if (canConnect != null) {
\r
623 connectionJudgment = canConnect.first;
\r
624 controlPoints.getLast().setPosition(ti.posDia).setAttachedToTerminal(ti);
\r
626 cp.getNode().showBranchPoint(isectPos);
\r
627 if (!endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me)))
\r
632 if (lastRouteGraphTarget != null) {
\r
633 lastRouteGraphTarget.getNode().showBranchPoint(null);
\r
634 lastRouteGraphTarget = null;
\r
639 connectionJudgment = null;
\r
640 if (isEndTerminalDefined()) {
\r
641 // CASE: Mouse was previously on top of a valid terminal to end
\r
642 // the connection. Now the mouse has been moved where there is
\r
643 // no longer a terminal to connect to.
\r
645 // => Disconnect the last edge segment from the previous
\r
646 // terminal, mark endElement/endTerminal non-existent
\r
647 // and connect the disconnected edge to a new branch point.
\r
649 controlPoints.getLast()
\r
650 .setPosition(mouseCanvasPos)
\r
651 .setDirection(calculateCurrentBranchPointDirection())
\r
652 .setAttachedToTerminal(null);
\r
654 endTerminal = null;
\r
656 // CASE: Mouse was not previously on top of a valid ending
\r
657 // element terminal.
\r
659 // => Move and re-orient last branch point.
\r
661 controlPoints.getLast()
\r
662 .setPosition(mouseCanvasPos)
\r
663 .setDirection(calculateCurrentBranchPointDirection());
\r
666 // Make sure that we are ending with a flag if ALT is pressed and no end
\r
667 // terminal is defined.
\r
668 if (!endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me)))
\r
674 protected boolean processMouseButtonPress(MouseButtonPressedEvent e) {
\r
675 MouseButtonEvent me = e;
\r
677 // Do nothing before the mouse has moved at least a little.
\r
678 // This prevents the user from ending the connection right where
\r
680 if (!mouseHasMoved)
\r
683 if (me.button == MouseEvent.LEFT_BUTTON) {
\r
684 Point2D mouseControlPos = me.controlPosition;
\r
685 Point2D mouseCanvasPos = util.getInverseTransform().transform(mouseControlPos, new Point2D.Double());
\r
687 ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);
\r
688 if (snapAdvisor != null)
\r
689 snapAdvisor.snap(mouseCanvasPos);
\r
691 if (lastRouteGraphTarget != null) {
\r
692 lastRouteGraphTarget.getNode().showBranchPoint(null);
\r
693 attachToConnection();
\r
696 } else if (isEndTerminalDefined()) {
\r
697 // Clicked on an allowed end terminal. End connection & end mode.
\r
698 createConnection();
\r
702 // Finish connection in thin air only if the
\r
703 // connection was started from a valid terminal.
\r
704 if (me.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK) && !startTerminals.isEmpty()) {
\r
705 Pair<ConnectionJudgement, TerminalInfo> pair = canConnect(null, null);
\r
706 if (pair != null) {
\r
707 connectionJudgment = (ConnectionJudgement) pair.first;
\r
708 selectedStartTerminal = pair.second;
\r
709 // endFlag = createFlag(EdgeEnd.End);
\r
710 // controlPoints.getLast().setAttachedToTerminal(endFlag);
\r
711 createConnection();
\r
715 // Inform the user why connection couldn't be created.
\r
716 String tmsg = terminalsToString(startTerminals);
\r
717 ErrorLogger.defaultLogWarning("Can't resolve connection type for new connection when starting from one of the following terminals:\n" + tmsg, null);
\r
720 } else if (routePointsAllowed()
\r
721 && (me.stateMask & (MouseEvent.ALT_MASK | MouseEvent.SHIFT_MASK | MouseEvent.CTRL_MASK)) == 0) {
\r
722 // Add new connection control point.
\r
723 controlPoints.add(newControlPointWithCalculatedDirection(mouseCanvasPos));
\r
724 resetForcedBranchPointDirection();
\r
729 // Eat the event to prevent other participants from doing
\r
730 // incompatible things while in this connection mode.
\r
732 } else if (me.button == MouseEvent.RIGHT_BUTTON) {
\r
733 return cancelPreviousBend();
\r
739 private void attachToConnection() {
\r
740 ConnectionJudgement judgment = this.connectionJudgment;
\r
741 if (judgment == null) {
\r
742 ErrorLogger.defaultLogError("Cannot attach to connection, no judgment available on connection validity", null);
\r
746 ConnectionBuilder builder = new ConnectionBuilder(this.diagram);
\r
747 RouteGraph before = lastRouteGraphTarget.getNode().getRouteGraph();
\r
748 THashMap<Object, Object> copyMap = new THashMap<>();
\r
749 RouteGraph after = before.copy(copyMap);
\r
751 RouteLine attachTo = (RouteLine) copyMap.get(lastRouteGraphTarget.getLine());
\r
752 after.makePersistent(attachTo);
\r
753 for (RouteLine line : after.getAllLines()) {
\r
754 if (!line.isTransient() && line.isHorizontal() == attachTo.isHorizontal()
\r
755 && line.getPosition() == attachTo.getPosition()) {
\r
760 RouteLine attachToLine = attachTo;
\r
761 RouteGraphDelta delta = new RouteGraphDelta(before, after);
\r
763 Simantics.getSession().asyncRequest(new WriteRequest() {
\r
765 public void perform(WriteGraph graph) throws DatabaseException {
\r
766 graph.markUndoPoint();
\r
767 Resource connection = ElementUtils.getObject(endTerminal.e);
\r
768 if (!delta.isEmpty()) {
\r
769 new RouteGraphConnection(graph, connection).synchronize(graph, before, after, delta);
\r
771 Resource line = RouteGraphConnection.deserialize(graph, attachToLine.getData());
\r
772 Deque<ControlPoint> cps = new ArrayDeque<>();
\r
773 for (Iterator<ControlPoint> iterator = controlPoints.descendingIterator(); iterator.hasNext();)
\r
774 cps.add(iterator.next());
\r
775 builder.attachToRouteGraph(graph, judgment, connection, line, cps, startTerminal, FlagClass.Type.In);
\r
777 }, new Callback<DatabaseException>() {
\r
779 public void run(DatabaseException parameter) {
\r
780 if (parameter != null)
\r
781 ExceptionUtils.logAndShowError(parameter);
\r
786 protected boolean cancelPreviousBend() {
\r
787 if (!routePointsAllowed())
\r
790 // Just to make this code more comprehensible, prevent an editing
\r
791 // case that requires ugly code to work.
\r
792 if (isEndingInFlag())
\r
795 // If there are no real route points, cancel whole connection.
\r
796 if (controlPoints.size() <= 2) {
\r
802 // Cancel last bend
\r
803 controlPoints.removeLast();
\r
804 controlPoints.getLast().setPosition(lastMouseCanvasPos);
\r
805 resetForcedBranchPointDirection();
\r
812 * Rotates the last branch point in the created connection in either
\r
813 * clockwise or counter-clockwise direction as a response to a user
\r
817 * At the same time it use {@link #forcedBranchPointDirection} to mark the
\r
818 * current last branch point to be forcefully oriented according to the
\r
819 * users wishes instead of calculating a default value for the orientation
\r
820 * from the routed connection path. See
\r
821 * {@link #calculateCurrentBranchPointDirection()} for more information on
\r
825 * The logic of this method goes as follows:
\r
827 * <li>Calculate the current branch point direction</li>
\r
828 * <li>If the branch point direction is currently user selected (
\r
829 * {@link #forcedBranchPointDirection}</li>
\r
835 * @return <code>true</code> if the rotation was successful
\r
837 protected boolean rotateLastBranchPoint(boolean clockwise) {
\r
838 Direction oldDir = calculateCurrentBranchPointDirection();
\r
840 if (forcedBranchPointDirection == null) {
\r
841 forcedBranchPointDirection = oldDir.toggleDetermined();
\r
843 forcedBranchPointDirection = clockwise ? oldDir.cycleNext() : oldDir.cyclePrevious();
\r
846 controlPoints.getLast().setDirection(forcedBranchPointDirection);
\r
854 * Set preferred direction for a branch/route point element.
\r
856 * @param branchPoint the element to set the direction for
\r
857 * @param direction the direction to set
\r
860 protected void setDirection(IElement branchPoint, Direction direction) {
\r
861 branchPoint.getElementClass().getSingleItem(BranchPoint.class).setDirectionPreference(branchPoint, direction);
\r
864 protected Direction forcedBranchPointDirection() {
\r
865 return forcedBranchPointDirection;
\r
868 protected void resetForcedBranchPointDirection() {
\r
869 forcedBranchPointDirection = null;
\r
872 protected void forceBranchPointDirection(Direction direction) {
\r
873 forcedBranchPointDirection = direction;
\r
879 protected Direction calculateCurrentBranchPointDirection() {
\r
880 // If this is not the first branch point, toggle direction compared to
\r
882 if (forcedBranchPointDirection != null)
\r
883 return forcedBranchPointDirection;
\r
885 if (controlPoints.size() > 2) {
\r
886 // This is not the first edge segment, toggle route point
\r
888 Iterator<ControlPoint> it = controlPoints.descendingIterator();
\r
890 ControlPoint secondLastCp = it.next();
\r
892 Direction dir = secondLastCp.getDirection();
\r
895 return Direction.Vertical;
\r
897 return Direction.Horizontal;
\r
902 // If this is the first branch point, calculate based on edge segment
\r
904 if (controlPoints.size() > 1) {
\r
905 Iterator<ControlPoint> it = controlPoints.descendingIterator();
\r
906 ControlPoint last = it.next();
\r
907 ControlPoint secondLast = it.next();
\r
909 double angle = Math.atan2(Math.abs(last.getPosition().getY() - secondLast.getPosition().getY()),
\r
910 Math.abs(last.getPosition().getX() - secondLast.getPosition().getX()));
\r
912 if (angle >= 0 && angle < Math.PI / 4) {
\r
913 return Direction.Horizontal;
\r
914 } else if (angle > Math.PI / 4 && angle <= Math.PI / 2) {
\r
915 return Direction.Vertical;
\r
919 return Direction.Any;
\r
922 protected boolean isEndingInFlag() {
\r
923 return endFlag != null;
\r
929 * @return <code>true</code> if updateSG was executed, <code>false</code>
\r
932 protected boolean endWithoutTerminal(Point2D mousePos, boolean altDown) {
\r
933 // Just go with branch points if flags are not allowed.
\r
937 boolean endTerminalDefined = isEndTerminalDefined();
\r
940 if (!isEndingInFlag()) {
\r
941 endFlag = createFlag(EdgeEnd.End);
\r
942 endFlagNode = showElement(ghostNode, "endFlag", endFlag.e, mousePos);
\r
943 controlPoints.getLast()
\r
944 .setDirection(calculateCurrentBranchPointDirection())
\r
945 .setAttachedToTerminal(endFlag);
\r
947 // TerminalPainter must refresh
\r
948 setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy);
\r
954 if (isEndingInFlag()) {
\r
955 // Currently ending with flag but ALT is no longer down
\r
956 // so that flag must be removed.
\r
958 endFlagNode.remove();
\r
959 endFlagNode = null;
\r
961 ControlPoint cp = controlPoints.getLast();
\r
962 cp.setDirection(calculateCurrentBranchPointDirection())
\r
963 .setAttachedToTerminal(endTerminal);
\r
965 if (endTerminalDefined) {
\r
966 cp.setPosition(endTerminal.posDia);
\r
968 cp.setPosition(mousePos);
\r
971 // Force TerminalPainter refresh
\r
972 setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy);
\r
981 protected void createConnection() {
\r
983 this.selectedStartTerminal,
\r
985 this.connectionJudgment,
\r
986 this.controlPoints);
\r
989 protected void createConnection(
\r
990 final TerminalInfo startTerminal,
\r
991 final TerminalInfo endTerminal,
\r
992 final ConnectionJudgement judgement,
\r
993 final Deque<ControlPoint> controlPoints)
\r
995 TimeLogger.resetTimeAndLog(getClass(), "createConnection");
\r
996 if (judgement == null) {
\r
997 // Inform the user why connection couldn't be created.
\r
998 String tmsg = terminalsToString(Arrays.asList(startTerminal, endTerminal));
\r
999 ErrorLogger.defaultLogError("Cannot create connection, no judgment available on connection validity when connecting the terminals:\n" + tmsg, null);
\r
1003 final ConnectionBuilder builder = new ConnectionBuilder(this.diagram);
\r
1005 Simantics.getSession().asyncRequest(new WriteRequest() {
\r
1007 public void perform(WriteGraph graph) throws DatabaseException {
\r
1008 builder.create(graph, judgement, controlPoints, startTerminal, endTerminal);
\r
1010 }, new Callback<DatabaseException>() {
\r
1012 public void run(DatabaseException parameter) {
\r
1013 if (parameter != null)
\r
1014 ExceptionUtils.logAndShowError(parameter);
\r
1020 * @param canvasPos
\r
1023 protected ControlPoint newControlPointWithCalculatedDirection(Point2D canvasPos) {
\r
1024 return new ControlPoint(canvasPos, calculateCurrentBranchPointDirection());
\r
1030 * @return <code>true</code> if the specified element terminal matches any
\r
1031 * TerminalInfo in {@link #startTerminals}
\r
1033 protected boolean isStartTerminal(IElement e, Terminal t) {
\r
1034 if (startTerminal == null)
\r
1036 for (TerminalInfo st : startTerminals) {
\r
1037 if (st.e == e && st.t == t) {
\r
1047 * @return <code>true</code> if the specified element terminal matches any
\r
1048 * TerminalInfo in {@link #startTerminals}
\r
1050 protected boolean containsStartTerminal(List<TerminalInfo> tis) {
\r
1051 if (startTerminal == null)
\r
1053 for (TerminalInfo st : startTerminals) {
\r
1054 for (TerminalInfo et : tis) {
\r
1055 if (st.e == et.e && st.t == et.t) {
\r
1063 protected static FlagClass.Type endToFlagType(EdgeEnd end) {
\r
1066 return FlagClass.Type.In;
\r
1068 return FlagClass.Type.Out;
\r
1070 throw new IllegalArgumentException("unrecognized edge end: " + end);
\r
1074 protected TerminalInfo createFlag(EdgeEnd connectionEnd) {
\r
1075 ElementClass flagClass = elementClassProvider.get(ElementClasses.FLAG);
\r
1076 IElement e = Element.spawnNew(flagClass);
\r
1078 e.setHint(FlagClass.KEY_FLAG_TYPE, endToFlagType(connectionEnd));
\r
1079 e.setHint(FlagClass.KEY_FLAG_MODE, FlagClass.Mode.Internal);
\r
1081 TerminalInfo ti = new TerminalInfo();
\r
1083 ti.t = ElementUtils.getSingleTerminal(e);
\r
1084 ti.posElem = TerminalUtil.getTerminalPosOnElement(e, ti.t);
\r
1085 ti.posDia = TerminalUtil.getTerminalPosOnDiagram(e, ti.t);
\r
1090 protected boolean shouldEndWithFlag(MouseEvent me) {
\r
1091 return shouldEndWithFlag( me.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK) );
\r
1094 protected boolean shouldEndWithFlag(boolean altPressed) {
\r
1095 return altPressed && !isEndTerminalDefined() && createFlags && startFlag == null;
\r
1098 protected boolean isEndTerminalDefined() {
\r
1099 return endTerminal != null;
\r
1102 protected boolean isFlagTerminal(TerminalInfo ti) {
\r
1103 return ti.e.getElementClass().containsClass(FlagHandler.class);
\r
1106 protected boolean allowReflexiveConnections() {
\r
1110 protected boolean routePointsAllowed() {
\r
1111 return Boolean.TRUE.equals(diagram.getHint(DiagramHints.KEY_ALLOW_ROUTE_POINTS));
\r
1115 * @param endElement
\r
1116 * @param endTerminal
\r
1119 @SuppressWarnings("unchecked")
\r
1120 protected final Pair<ConnectionJudgement, TerminalInfo> canConnect(IElement endElement, Terminal endTerminal) {
\r
1121 IConnectionAdvisor advisor = diagram.getHint(DiagramHints.CONNECTION_ADVISOR);
\r
1122 Object judgement = canConnect(advisor, endElement, endTerminal);
\r
1123 if (judgement == null)
\r
1125 if (judgement instanceof Pair<?, ?>)
\r
1126 return (Pair<ConnectionJudgement, TerminalInfo>) judgement;
\r
1127 return Pair.<ConnectionJudgement, TerminalInfo>make((ConnectionJudgement) judgement, startTerminal);
\r
1130 protected Object canConnect(IConnectionAdvisor advisor, IElement endElement, Terminal endTerminal) {
\r
1131 if (advisor == null)
\r
1132 return Pair.make(ConnectionJudgement.CANBEMADELEGAL, startTerminal);
\r
1133 if (startTerminals.isEmpty()) {
\r
1134 ConnectionJudgement obj = (ConnectionJudgement) advisor.canBeConnected(null, null, null, endElement, endTerminal);
\r
1135 return obj != null ? Pair.<ConnectionJudgement, TerminalInfo>make(obj, null) : null;
\r
1137 for (TerminalInfo st : startTerminals) {
\r
1138 ConnectionJudgement obj = (ConnectionJudgement) advisor.canBeConnected(null, st.e, st.t, endElement, endTerminal);
\r
1139 if (obj != null) {
\r
1140 return Pair.make(obj, st);
\r
1147 * For generating debugging information of what was attempted by the user
\r
1148 * when a connection couldn't be created.
\r
1153 private String terminalsToString(final Iterable<TerminalInfo> ts) {
\r
1155 return Simantics.sync(new UniqueRead<String>() {
\r
1157 public String perform(ReadGraph graph) throws DatabaseException {
\r
1158 DiagramResource DIA = DiagramResource.getInstance(graph);
\r
1159 ModelingResources MOD = ModelingResources.getInstance(graph);
\r
1160 StringBuilder sb = new StringBuilder();
\r
1161 boolean first = true;
\r
1162 for (TerminalInfo ti : ts) {
\r
1166 sb.append("element ");
\r
1167 Object o = ElementUtils.getObject(ti.e);
\r
1168 if (o instanceof Resource) {
\r
1169 Resource er = (Resource) o;
\r
1170 Resource cer = graph.getPossibleObject(er, MOD.ElementToComponent);
\r
1171 Resource r = cer != null ? cer : er;
\r
1172 sb.append(NameUtils.getSafeName(graph, r)).append(" : ");
\r
1173 for (Resource type : graph.getPrincipalTypes(r)) {
\r
1174 sb.append(NameUtils.getSafeName(graph, type, true));
\r
1177 sb.append(ti.e.toString());
\r
1179 sb.append(", terminal ");
\r
1180 if (ti.t instanceof ResourceTerminal) {
\r
1181 Resource tr = ((ResourceTerminal) ti.t).getResource();
\r
1182 Resource cp = graph.getPossibleObject(tr, DIA.HasConnectionPoint);
\r
1183 Resource r = cp != null ? cp : tr;
\r
1184 sb.append(NameUtils.getSafeName(graph, r, true));
\r
1186 sb.append(ti.t.toString());
\r
1189 return sb.toString();
\r
1192 } catch (DatabaseException e) {
\r
1193 return e.getMessage();
\r