X-Git-Url: https://gerrit.simantics.org/r/gitweb?p=simantics%2Fplatform.git;a=blobdiff_plain;f=bundles%2Forg.simantics.diagram%2Fsrc%2Forg%2Fsimantics%2Fdiagram%2Fparticipant%2FConnectTool2.java;h=38bdc706cb88e8b20a959b03d18bfac02ec17de9;hp=71a8d28290f9b7e020ef3bbb368ccc994287f8bc;hb=05690e88acf9883a20f1480d4636c5cd037b49ea;hpb=bd5bc6e45f700e755b61bd112631796631330ecb diff --git a/bundles/org.simantics.diagram/src/org/simantics/diagram/participant/ConnectTool2.java b/bundles/org.simantics.diagram/src/org/simantics/diagram/participant/ConnectTool2.java index 71a8d2829..38bdc706c 100644 --- a/bundles/org.simantics.diagram/src/org/simantics/diagram/participant/ConnectTool2.java +++ b/bundles/org.simantics.diagram/src/org/simantics/diagram/participant/ConnectTool2.java @@ -1,1197 +1,1191 @@ -/******************************************************************************* - * 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.MouseMovedEvent; -import org.simantics.scenegraph.g2d.events.command.CommandEvent; -import org.simantics.scenegraph.g2d.events.command.Commands; -import org.simantics.scenegraph.g2d.nodes.BranchPointNode; -import org.simantics.scenegraph.g2d.nodes.ShapeNode; -import org.simantics.scenegraph.g2d.snap.ISnapAdvisor; -import org.simantics.scenegraph.utils.GeometryUtils; -import org.simantics.scenegraph.utils.Quality; -import org.simantics.structural2.modelingRules.ConnectionJudgement; -import org.simantics.utils.datastructures.Callback; -import org.simantics.utils.datastructures.Pair; -import org.simantics.utils.logging.TimeLogger; -import org.simantics.utils.ui.ErrorLogger; -import org.simantics.utils.ui.ExceptionUtils; - -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. - * - *

- * 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. null if connection - * was started from a flag or a branch point. - * - * The value is received by the constructor. - */ - protected List startTerminals; - - /** - * Refers to any of the possible overlapping start terminals. The value is - * taken from the first index of {@link #startTerminals} assuming that the - * first one is the nearest. It is null if - * {@link #startTerminals} is empty. - */ - protected TerminalInfo startTerminal; - - protected TerminalInfo startFlag; - - /** - * Starting position of the connection, received as an external argument. - */ - protected final Point2D startPos; - - /** - * true if this tool should create connection continuation - * flags, false otherwise. - */ - protected boolean createFlags; - - /** - * - */ - protected IElementClassProvider elementClassProvider; - - /** - * - */ - protected Deque controlPoints = new ArrayDeque(); - - /** - * Contains null when a connection is started from a new flag - * or one of the terminals in {@link #startTerminals} when a connection is - * being created starting from a terminal or possibly a set of terminals. - * - *

- * Note that this is different from {@link #startTerminal} which simply - * represents the first element of {@link #startTerminals}. - * - *

- * Only when this value and {@link #endTerminal} is properly set will a - * connection be created between two element terminals. - */ - protected TerminalInfo selectedStartTerminal; - - /** - * Element terminal of connection end element. null if - * connection cannot be ended where it is currently being attempted to end. - */ - protected TerminalInfo endTerminal; - - /** - * The latest connectability judgment from the active - * {@link IConnectionAdvisor} should the connection happen between - * {@link #selectedStartTerminal} and {@link #endTerminal}. - */ - protected ConnectionJudgement connectionJudgment; - - /** - * If non-null during connection drawing this field tells the direction - * forced for the current branch point by the user through the UI commands - * {@link Commands#ROTATE_ELEMENT_CCW} and - * {@link Commands#ROTATE_ELEMENT_CW}. - */ - private Direction forcedBranchPointDirection; - - /** - * A temporary variable for use with - * {@link TerminalTopology#getTerminals(IElement, Collection)}. - */ - protected Collection terminals = new ArrayList(); - - /** - * Previous mouse canvas position recorded by - * {@link #processMouseMove(MouseMovedEvent)}. - */ - protected Point2D lastMouseCanvasPos = new Point2D.Double(); - - /** - * Set to true once {@link #processMouseMove(MouseMovedEvent)} has been - * invoked at least once. This is used to tell whether to allow creation of - * branch points or finising the connection in thin air. It will not be - * allowed if the mouse has not moved at all since starting the connection. - */ - protected boolean mouseHasMoved = false; - - protected TerminalHoverStrategy originalStrategy = null; - - protected TerminalHoverStrategy terminalHoverStrategy = new TerminalHoverStrategy() { - @Override - public boolean highlightEnabled() { - return !isEndingInFlag(); - } - - @Override - public boolean highlight(TerminalInfo ti) { - boolean reflexive = isStartTerminal(ti.e, ti.t); - if (reflexive && !allowReflexiveConnections()) - return false; - - return canConnect(ti.e, ti.t) != null; - } - }; - - protected final static Composite ALPHA_COMPOSITE = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.75f); - - /** - * Root scene graph node for all visualization performed by this tool. - */ - protected G2DParentNode ghostNode; - - /** - * Indicates whether the connection is about to be ended into a new - * flag/branchpoint or not. - */ - protected TerminalInfo endFlag; - - protected G2DParentNode endFlagNode; - - private RouteGraphTarget lastRouteGraphTarget; - - /** - * @param startTerminal - * @param mouseId - * @param startCanvasPos - */ - public ConnectTool2(TerminalInfo startTerminal, int mouseId, Point2D startCanvasPos) { - this(startTerminal == null ? Collections. emptyList() - : Collections.singletonList(startTerminal), - mouseId, - startCanvasPos); - } - - /** - * @param startTerminals - * @param mouseId - * @param startCanvasPos - */ - public ConnectTool2(List startTerminals, int mouseId, Point2D startCanvasPos) { - super(mouseId); - - if (startCanvasPos == null) - throw new NullPointerException("null start position"); - if (startTerminals == null) - throw new NullPointerException("null start terminals"); - - this.startPos = startCanvasPos; - this.lastMouseCanvasPos.setLocation(startPos); - - this.startTerminals = startTerminals; - this.startTerminal = startTerminals.isEmpty() ? null : startTerminals.get(0); - } - - @Override - public void addedToContext(ICanvasContext ctx) { - super.addedToContext(ctx); - - if (quality != null) - quality.setStaticQuality(Quality.LOW); - - // Force terminals to always be highlighted without pressing certain - // keys or key combinations. - originalStrategy = getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY); - setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy); - } - - @Override - protected void onDiagramSet(IDiagram newDiagram, IDiagram oldDiagram) { - if (newDiagram != null) { - // Get IElementClassProvider - ISynchronizationContext ctx = newDiagram.getHint(SynchronizationHints.CONTEXT); - if (ctx != null) { - this.elementClassProvider = ctx.get(SynchronizationHints.ELEMENT_CLASS_PROVIDER); - } - - // See if flags should be created or not. - this.createFlags = Boolean.TRUE.equals(newDiagram.getHint(DiagramHints.KEY_USE_CONNECTION_FLAGS)); - startConnection(); - } - } - - @Override - public void removedFromContext(ICanvasContext ctx) { - if (getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY) == terminalHoverStrategy) { - if (originalStrategy != null) - setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, originalStrategy); - else - removeHint(TerminalPainter.TERMINAL_HOVER_STRATEGY); - } - - if (quality != null) - quality.setStaticQuality(null); - - super.removedFromContext(ctx); - } - - protected void startConnection() { - Point2D startPos = (Point2D) this.startPos.clone(); - ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR); - if (snapAdvisor != null) - snapAdvisor.snap(startPos); - - // Resolve the first element and terminal of the connection. - ControlPoint start = new ControlPoint(startPos); - - if (startTerminal != null) { - assert ElementUtils.peekDiagram(startTerminal.e) == diagram; - Point2D terminalPos = new Point2D.Double(startTerminal.posDia.getTranslateX(), - startTerminal.posDia.getTranslateY()); - start.setPosition(terminalPos).setAttachedToTerminal(startTerminal); - } else { - // Create TerminalInfo describing the flag to be created. - if (createFlags) { - // This prevents connection creation from creating a branch - // point in place of this flag. - startFlag = createFlag(EdgeEnd.Begin); - start.setAttachedToTerminal(startFlag); - showElement(ghostNode, "startFlag", startFlag.e, startPos); - } - } - controlPoints.add(start); - controlPoints.add(new ControlPoint(startPos)); - - // Make sure that we are ending with a flag if ALT is pressed. - // This makes the tool always start with a flag which can be quite - // cumbersome and is therefore disabled. The current version will not - // end the connection if the mouse has not moved at all. - //if (keyUtil.isKeyPressed(java.awt.event.KeyEvent.VK_ALT)) { - // endWithoutTerminal(lastMouseCanvasPos, true); - //} - } - - @SGInit - public void initSG(G2DParentNode parent) { - ghostNode = parent.addNode(G2DParentNode.class); - ghostNode.setZIndex(PAINT_PRIORITY); - - ShapeNode pathNode = ghostNode.getOrCreateNode("path", ShapeNode.class); - pathNode.setColor(new Color(160, 0, 0)); - pathNode.setStroke(new BasicStroke(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(); - } - - static class Segment { - public final ControlPoint begin; - public final ControlPoint end; - public Path2D path; - - public Segment(ControlPoint begin, ControlPoint end) { - this.begin = begin; - this.end = end; - } - - @Override - public String toString() { - return "Segment[begin=" + begin + ", end=" + end + ", path=" + path + "]"; - } - } - - private RouteTerminal addControlPoint(RouteGraph routeGraph, ControlPoint cp) { - TerminalInfo ti = cp.getAttachedTerminal(); - if(ti != null && ti != startFlag && ti != endFlag) { - Rectangle2D bounds = ElementUtils.getElementBoundsOnDiagram(ti.e, new Rectangle2D.Double()); - GeometryUtils.expandRectangle(bounds, 2); - int allowedDirections = RouteGraphConnectionClass.shortestDirectionOutOfBounds( - ti.posDia.getTranslateX(), ti.posDia.getTranslateY(), bounds); - return routeGraph.addTerminal(ti.posDia.getTranslateX(), ti.posDia.getTranslateY(), - bounds, allowedDirections, PlainLineEndStyle.INSTANCE); - } - else { - double x = cp.getPosition().getX(); - double y = cp.getPosition().getY(); - int allowedDirections = 0xf; - switch(cp.getDirection()) { - case Horizontal: allowedDirections = 5; break; - case Vertical: allowedDirections = 10; break; - case Any: allowedDirections = 15; break; - } - return routeGraph.addTerminal(x, y, x, y, x, y, allowedDirections); - } - } - - protected void updateSG() { - if (controlPoints.size() != 2) - return; - - ControlPoint begin = controlPoints.getFirst(); - ControlPoint end = controlPoints.getLast(); - - RouteGraph routeGraph = new RouteGraph(); - RouteTerminal a = addControlPoint(routeGraph, begin); - RouteTerminal b = addControlPoint(routeGraph, end); - routeGraph.link(a, b); - - 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; - } - - private List toSegments(Deque points) { - if (points.isEmpty()) - return Collections.emptyList(); - - List segments = new ArrayList(); - - Iterator it = points.iterator(); - ControlPoint prev = it.next(); - while (it.hasNext()) { - ControlPoint next = it.next(); - segments.add(new Segment(prev, next)); - prev = next; - } - - return segments; - } - - @SGCleanup - public void cleanupSG() { - ghostNode.remove(); - ghostNode = null; - } - - @EventHandler(priority = 200) - public boolean handleCommandEvents(CommandEvent ce) { - if (ce.command.equals(Commands.CANCEL)) { - setDirty(); - remove(); - return true; - } else if (ce.command.equals(Commands.ROTATE_ELEMENT_CCW) || ce.command.equals(Commands.ROTATE_ELEMENT_CW)) { - return rotateLastBranchPoint(ce.command.equals(Commands.ROTATE_ELEMENT_CW)); - } - return false; - } - - @EventHandler(priority = PointerInteractor.TOOL_PRIORITY + 20) - public boolean handleKeyEvents(KeyEvent ke) { - if (ke instanceof KeyPressedEvent) { - // Back-space, cancel prev bend - if (ke.keyCode == java.awt.event.KeyEvent.VK_BACK_SPACE) - return cancelPreviousBend(); - } - - if (ke.keyCode == java.awt.event.KeyEvent.VK_ALT) { - if (createFlags) { - endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(ke instanceof KeyPressedEvent)); - return true; - } - } - - return false; - } - - @EventHandler(priority = PointerInteractor.TOOL_PRIORITY + 20) - public boolean handleEvent(MouseEvent me) { - // Only handle events for the connection-initiating mouse - if (me.mouseId != mouseId) - return false; - - if (me instanceof MouseMovedEvent) - return processMouseMove((MouseMovedEvent) me); - - if (me instanceof MouseButtonPressedEvent) - return processMouseButtonPress((MouseButtonPressedEvent) me); - - return false; - } - - protected boolean processMouseMove(MouseMovedEvent me) { - mouseHasMoved = true; - - Point2D mouseControlPos = me.controlPosition; - Point2D mouseCanvasPos = util.controlToCanvas(mouseControlPos, new Point2D.Double()); - - ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR); - if (snapAdvisor != null) - snapAdvisor.snap(mouseCanvasPos); - - // Record last snapped canvas position of mouse. - this.lastMouseCanvasPos.setLocation(mouseCanvasPos); - - if (isEndingInFlag()) { - endFlagNode.setTransform(AffineTransform.getTranslateInstance(mouseCanvasPos.getX(), mouseCanvasPos.getY())); - } - - List tis = pi.pickTerminals(me.controlPosition); - tis = TerminalUtil.findNearestOverlappingTerminals(tis); - if (!tis.isEmpty() && !containsStartTerminal(tis)) { - //System.out.println("end terminals (" + tis.size() + "):\n" + EString.implode(tis)); - for (TerminalInfo ti : tis) { - Pair canConnect = canConnect(ti.e, ti.t); - if (canConnect != null) { - connectionJudgment = canConnect.first; - - if (!isEndingInFlag() || !TerminalUtil.isSameTerminal(ti, endTerminal)) { - if (canConnect.second != null) { - controlPoints.getFirst() - .setPosition(canConnect.second.posDia) - .setAttachedToTerminal(canConnect.second); - } - controlPoints.getLast() - .setPosition(ti.posDia) - .setAttachedToTerminal(ti); - - selectedStartTerminal = canConnect.second; - endTerminal = ti; - } - - // Make sure that we are ending with a flag if ALT is pressed - // and no end terminal is defined. - if (!endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me))) - updateSG(); - return false; - } - } - } else { - RouteGraphTarget cp = RouteGraphConnectTool.pickRouteGraphConnection( - 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 canConnect = canConnect(ti.e, ti.t); - if (canConnect != null) { - connectionJudgment = 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; - 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 (lastRouteGraphTarget != null) { - lastRouteGraphTarget.getNode().showBranchPoint(null); - attachToConnection(); - remove(); - return true; - } else if (isEndTerminalDefined()) { - // Clicked on an allowed end terminal. End connection & end mode. - createConnection(); - remove(); - return true; - } else { - // Finish connection in thin air only if the - // connection was started from a valid terminal. - if (me.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK) && !startTerminals.isEmpty()) { - Pair pair = canConnect(null, null); - if (pair != null) { - connectionJudgment = (ConnectionJudgement) pair.first; - selectedStartTerminal = pair.second; -// endFlag = createFlag(EdgeEnd.End); -// controlPoints.getLast().setAttachedToTerminal(endFlag); - createConnection(); - setDirty(); - remove(); - } else { - // Inform the user why connection couldn't be created. - String tmsg = terminalsToString(startTerminals); - ErrorLogger.defaultLogWarning("Can't resolve connection type for new connection when starting from one of the following terminals:\n" + tmsg, null); - } - return true; - } else if (routePointsAllowed() - && (me.stateMask & (MouseEvent.ALT_MASK | MouseEvent.SHIFT_MASK | MouseEvent.CTRL_MASK)) == 0) { - // Add new connection control point. - controlPoints.add(newControlPointWithCalculatedDirection(mouseCanvasPos)); - resetForcedBranchPointDirection(); - updateSG(); - } - } - - // Eat the event to prevent other participants from doing - // incompatible things while in this connection mode. - return true; - } else if (me.button == MouseEvent.RIGHT_BUTTON) { - return cancelPreviousBend(); - } - - return false; - } - - private void attachToConnection() { - ConnectionJudgement judgment = this.connectionJudgment; - 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 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 cps = new ArrayDeque<>(); - for (Iterator iterator = controlPoints.descendingIterator(); iterator.hasNext();) - cps.add(iterator.next()); - builder.attachToRouteGraph(graph, judgment, connection, line, cps, startTerminal, FlagClass.Type.In); - } - }, new Callback() { - @Override - public void run(DatabaseException 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. - * - *

- * At the same time it use {@link #forcedBranchPointDirection} to mark the - * current last branch point to be forcefully oriented according to the - * users wishes instead of calculating a default value for the orientation - * from the routed connection path. See - * {@link #calculateCurrentBranchPointDirection()} for more information on - * this. - * - *

- * The logic of this method goes as follows: - *

- * - * @param clockwise - * @return true if the rotation was successful - */ - protected boolean rotateLastBranchPoint(boolean clockwise) { - Direction oldDir = calculateCurrentBranchPointDirection(); - - if (forcedBranchPointDirection == null) { - forcedBranchPointDirection = oldDir.toggleDetermined(); - } else { - forcedBranchPointDirection = clockwise ? oldDir.cycleNext() : oldDir.cyclePrevious(); - } - - controlPoints.getLast().setDirection(forcedBranchPointDirection); - - updateSG(); - - return true; - } - - /** - * Set preferred direction for a branch/route point element. - * - * @param branchPoint the element to set the direction for - * @param direction the direction to set - * @return - */ - protected void setDirection(IElement branchPoint, Direction direction) { - branchPoint.getElementClass().getSingleItem(BranchPoint.class).setDirectionPreference(branchPoint, direction); - } - - protected Direction forcedBranchPointDirection() { - return forcedBranchPointDirection; - } - - protected void resetForcedBranchPointDirection() { - forcedBranchPointDirection = null; - } - - protected void forceBranchPointDirection(Direction direction) { - forcedBranchPointDirection = direction; - } - - /** - * @return - */ - protected Direction calculateCurrentBranchPointDirection() { - // If this is not the first branch point, toggle direction compared to - // last. - if (forcedBranchPointDirection != null) - return forcedBranchPointDirection; - - if (controlPoints.size() > 2) { - // This is not the first edge segment, toggle route point - // directions. - Iterator it = controlPoints.descendingIterator(); - it.next(); - ControlPoint secondLastCp = it.next(); - - Direction dir = secondLastCp.getDirection(); - switch (dir) { - case Horizontal: - return Direction.Vertical; - case Vertical: - return Direction.Horizontal; - case Any: - } - } - - // If this is the first branch point, calculate based on edge segment - // angle. - if (controlPoints.size() > 1) { - Iterator it = controlPoints.descendingIterator(); - ControlPoint last = it.next(); - ControlPoint secondLast = it.next(); - - double angle = Math.atan2(Math.abs(last.getPosition().getY() - secondLast.getPosition().getY()), - Math.abs(last.getPosition().getX() - secondLast.getPosition().getX())); - - if (angle >= 0 && angle < Math.PI / 4) { - return Direction.Horizontal; - } else if (angle > Math.PI / 4 && angle <= Math.PI / 2) { - return Direction.Vertical; - } - } - - return Direction.Any; - } - - protected boolean isEndingInFlag() { - return endFlag != null; - } - - /** - * @param mousePos - * @param altDown - * @return true if updateSG was executed, false - * 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 controlPoints) - { - TimeLogger.resetTimeAndLog(getClass(), "createConnection"); - if (judgement == null) { - // Inform the user why connection couldn't be created. - String tmsg = terminalsToString(Arrays.asList(startTerminal, endTerminal)); - ErrorLogger.defaultLogError("Cannot create connection, no judgment available on connection validity when connecting the terminals:\n" + tmsg, null); - return; - } - - final ConnectionBuilder builder = new ConnectionBuilder(this.diagram); - - Simantics.getSession().asyncRequest(new WriteRequest() { - @Override - public void perform(WriteGraph graph) throws DatabaseException { - builder.create(graph, judgement, controlPoints, startTerminal, endTerminal); - } - }, new Callback() { - @Override - public void run(DatabaseException parameter) { - if (parameter != null) - ExceptionUtils.logAndShowError(parameter); - } - }); - } - - /** - * @param canvasPos - * @return - */ - protected ControlPoint newControlPointWithCalculatedDirection(Point2D canvasPos) { - return new ControlPoint(canvasPos, calculateCurrentBranchPointDirection()); - } - - /** - * @param e - * @param t - * @return true if the specified element terminal matches any - * TerminalInfo in {@link #startTerminals} - */ - protected boolean isStartTerminal(IElement e, Terminal t) { - if (startTerminal == null) - return false; - for (TerminalInfo st : startTerminals) { - if (st.e == e && st.t == t) { - return true; - } - } - return false; - } - - /** - * @param e - * @param t - * @return true if the specified element terminal matches any - * TerminalInfo in {@link #startTerminals} - */ - protected boolean containsStartTerminal(List tis) { - if (startTerminal == null) - return false; - for (TerminalInfo st : startTerminals) { - for (TerminalInfo et : tis) { - if (st.e == et.e && st.t == et.t) { - return true; - } - } - } - return false; - } - - protected static FlagClass.Type endToFlagType(EdgeEnd end) { - switch (end) { - case Begin: - return FlagClass.Type.In; - case End: - return FlagClass.Type.Out; - default: - throw new IllegalArgumentException("unrecognized edge end: " + end); - } - } - - protected TerminalInfo createFlag(EdgeEnd connectionEnd) { - ElementClass flagClass = elementClassProvider.get(ElementClasses.FLAG); - IElement e = Element.spawnNew(flagClass); - - e.setHint(FlagClass.KEY_FLAG_TYPE, endToFlagType(connectionEnd)); - e.setHint(FlagClass.KEY_FLAG_MODE, FlagClass.Mode.Internal); - - TerminalInfo ti = new TerminalInfo(); - ti.e = e; - ti.t = ElementUtils.getSingleTerminal(e); - ti.posElem = TerminalUtil.getTerminalPosOnElement(e, ti.t); - ti.posDia = TerminalUtil.getTerminalPosOnDiagram(e, ti.t); - - return ti; - } - - protected boolean shouldEndWithFlag(MouseEvent me) { - return shouldEndWithFlag( me.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK) ); - } - - protected boolean shouldEndWithFlag(boolean altPressed) { - return altPressed && !isEndTerminalDefined() && createFlags && startFlag == null; - } - - protected boolean isEndTerminalDefined() { - return endTerminal != null; - } - - protected boolean isFlagTerminal(TerminalInfo ti) { - return ti.e.getElementClass().containsClass(FlagHandler.class); - } - - protected boolean allowReflexiveConnections() { - return false; - } - - protected boolean routePointsAllowed() { - return Boolean.TRUE.equals(diagram.getHint(DiagramHints.KEY_ALLOW_ROUTE_POINTS)); - } - - /** - * @param endElement - * @param endTerminal - * @return - */ - @SuppressWarnings("unchecked") - protected final Pair canConnect(IElement endElement, Terminal endTerminal) { - IConnectionAdvisor advisor = diagram.getHint(DiagramHints.CONNECTION_ADVISOR); - Object judgement = canConnect(advisor, endElement, endTerminal); - if (judgement == null) - return null; - if (judgement instanceof Pair) - return (Pair) judgement; - return Pair.make((ConnectionJudgement) judgement, startTerminal); - } - - protected Object canConnect(IConnectionAdvisor advisor, IElement endElement, Terminal endTerminal) { - if (advisor == null) - return Pair.make(ConnectionJudgement.CANBEMADELEGAL, startTerminal); - if (startTerminals.isEmpty()) { - ConnectionJudgement obj = (ConnectionJudgement) advisor.canBeConnected(null, null, null, endElement, endTerminal); - return obj != null ? Pair.make(obj, null) : null; - } - for (TerminalInfo st : startTerminals) { - ConnectionJudgement obj = (ConnectionJudgement) advisor.canBeConnected(null, st.e, st.t, endElement, endTerminal); - if (obj != null) { - return Pair.make(obj, st); - } - } - return null; - } - - /** - * For generating debugging information of what was attempted by the user - * when a connection couldn't be created. - * - * @param ts - * @return - */ - private String terminalsToString(final Iterable ts) { - try { - return Simantics.sync(new UniqueRead() { - @Override - public String perform(ReadGraph graph) throws DatabaseException { - DiagramResource DIA = DiagramResource.getInstance(graph); - ModelingResources MOD = ModelingResources.getInstance(graph); - StringBuilder sb = new StringBuilder(); - boolean first = true; - for (TerminalInfo ti : ts) { - if (!first) - sb.append("\n"); - first = false; - sb.append("element "); - Object o = ElementUtils.getObject(ti.e); - if (o instanceof Resource) { - Resource er = (Resource) o; - Resource cer = graph.getPossibleObject(er, MOD.ElementToComponent); - Resource r = cer != null ? cer : er; - sb.append(NameUtils.getSafeName(graph, r)).append(" : "); - for (Resource type : graph.getPrincipalTypes(r)) { - sb.append(NameUtils.getSafeName(graph, type, true)); - } - } else { - sb.append(ti.e.toString()); - } - sb.append(", terminal "); - if (ti.t instanceof ResourceTerminal) { - Resource tr = ((ResourceTerminal) ti.t).getResource(); - Resource cp = graph.getPossibleObject(tr, DIA.HasConnectionPoint); - Resource r = cp != null ? cp : tr; - sb.append(NameUtils.getSafeName(graph, r, true)); - } else { - sb.append(ti.t.toString()); - } - } - return sb.toString(); - } - }); - } catch (DatabaseException e) { - return e.getMessage(); - } - } - +/******************************************************************************* + * 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. + * + *

+ * 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. null if connection + * was started from a flag or a branch point. + * + * The value is received by the constructor. + */ + protected List startTerminals; + + /** + * Refers to any of the possible overlapping start terminals. The value is + * taken from the first index of {@link #startTerminals} assuming that the + * first one is the nearest. It is null if + * {@link #startTerminals} is empty. + */ + protected TerminalInfo startTerminal; + + protected TerminalInfo startFlag; + + /** + * Starting position of the connection, received as an external argument. + */ + protected final Point2D startPos; + + /** + * true if this tool should create connection continuation + * flags, false otherwise. + */ + protected boolean createFlags; + + /** + * + */ + protected IElementClassProvider elementClassProvider; + + /** + * + */ + protected Deque controlPoints = new ArrayDeque(); + + /** + * Contains null when a connection is started from a new flag + * or one of the terminals in {@link #startTerminals} when a connection is + * being created starting from a terminal or possibly a set of terminals. + * + *

+ * Note that this is different from {@link #startTerminal} which simply + * represents the first element of {@link #startTerminals}. + * + *

+ * Only when this value and {@link #endTerminal} is properly set will a + * connection be created between two element terminals. + */ + protected TerminalInfo selectedStartTerminal; + + /** + * Element terminal of connection end element. null if + * connection cannot be ended where it is currently being attempted to end. + */ + protected TerminalInfo endTerminal; + + /** + * The latest connectability judgment from the active + * {@link IConnectionAdvisor} should the connection happen between + * {@link #selectedStartTerminal} and {@link #endTerminal}. + */ + protected ConnectionJudgement connectionJudgment; + + /** + * 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 terminals = new ArrayList(); + + /** + * Previous mouse canvas position recorded by + * {@link #processMouseMove(MouseMovedEvent)}. + */ + protected Point2D lastMouseCanvasPos = new Point2D.Double(); + + /** + * Set to true once {@link #processMouseMove(MouseMovedEvent)} has been + * invoked at least once. This is used to tell whether to allow creation of + * branch points or finising the connection in thin air. It will not be + * allowed if the mouse has not moved at all since starting the connection. + */ + protected boolean mouseHasMoved = false; + + protected TerminalHoverStrategy originalStrategy = null; + + protected TerminalHoverStrategy terminalHoverStrategy = new TerminalHoverStrategy() { + @Override + public boolean highlightEnabled() { + return !isEndingInFlag(); + } + + @Override + public boolean highlight(TerminalInfo ti) { + boolean reflexive = isStartTerminal(ti.e, ti.t); + if (reflexive && !allowReflexiveConnections()) + return false; + + return canConnect(ti.e, ti.t) != null; + } + }; + + protected final static Composite ALPHA_COMPOSITE = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.75f); + + /** + * Root scene graph node for all visualization performed by this tool. + */ + protected G2DParentNode ghostNode; + + /** + * Indicates whether the connection is about to be ended into a new + * flag/branchpoint or not. + */ + protected TerminalInfo endFlag; + + protected G2DParentNode endFlagNode; + + private RouteGraphTarget lastRouteGraphTarget; + + /** + * @param startTerminal + * @param mouseId + * @param startCanvasPos + */ + public ConnectTool2(TerminalInfo startTerminal, int mouseId, Point2D startCanvasPos) { + this(startTerminal == null ? Collections. emptyList() + : Collections.singletonList(startTerminal), + mouseId, + startCanvasPos); + } + + /** + * @param startTerminals + * @param mouseId + * @param startCanvasPos + */ + public ConnectTool2(List startTerminals, int mouseId, Point2D startCanvasPos) { + super(mouseId); + + if (startCanvasPos == null) + throw new NullPointerException("null start position"); + if (startTerminals == null) + throw new NullPointerException("null start terminals"); + + this.startPos = startCanvasPos; + this.lastMouseCanvasPos.setLocation(startPos); + + this.startTerminals = startTerminals; + this.startTerminal = startTerminals.isEmpty() ? null : startTerminals.get(0); + } + + @Override + public void addedToContext(ICanvasContext ctx) { + super.addedToContext(ctx); + + if (quality != null) + quality.setStaticQuality(Quality.LOW); + + // Force terminals to always be highlighted without pressing certain + // keys or key combinations. + originalStrategy = getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY); + setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy); + } + + @Override + protected void onDiagramSet(IDiagram newDiagram, IDiagram oldDiagram) { + if (newDiagram != null) { + // Get IElementClassProvider + ISynchronizationContext ctx = newDiagram.getHint(SynchronizationHints.CONTEXT); + if (ctx != null) { + this.elementClassProvider = ctx.get(SynchronizationHints.ELEMENT_CLASS_PROVIDER); + } + + // See if flags should be created or not. + this.createFlags = Boolean.TRUE.equals(newDiagram.getHint(DiagramHints.KEY_USE_CONNECTION_FLAGS)); + startConnection(); + } + } + + @Override + public void removedFromContext(ICanvasContext ctx) { + if (getHint(TerminalPainter.TERMINAL_HOVER_STRATEGY) == terminalHoverStrategy) { + if (originalStrategy != null) + setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, originalStrategy); + else + removeHint(TerminalPainter.TERMINAL_HOVER_STRATEGY); + } + + if (quality != null) + quality.setStaticQuality(null); + + super.removedFromContext(ctx); + } + + protected void startConnection() { + Point2D startPos = (Point2D) this.startPos.clone(); + ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR); + if (snapAdvisor != null) + snapAdvisor.snap(startPos); + + // Resolve the first element and terminal of the connection. + ControlPoint start = new ControlPoint(startPos); + + if (startTerminal != null) { + assert ElementUtils.peekDiagram(startTerminal.e) == diagram; + Point2D terminalPos = new Point2D.Double(startTerminal.posDia.getTranslateX(), + startTerminal.posDia.getTranslateY()); + start.setPosition(terminalPos).setAttachedToTerminal(startTerminal); + } else { + // Create TerminalInfo describing the flag to be created. + if (createFlags) { + // This prevents connection creation from creating a branch + // point in place of this flag. + startFlag = createFlag(EdgeEnd.Begin); + start.setAttachedToTerminal(startFlag); + showElement(ghostNode, "startFlag", startFlag.e, startPos); + } + } + controlPoints.add(start); + controlPoints.add(new ControlPoint(startPos)); + + // Make sure that we are ending with a flag if ALT is pressed. + // This makes the tool always start with a flag which can be quite + // cumbersome and is therefore disabled. The current version will not + // end the connection if the mouse has not moved at all. + //if (keyUtil.isKeyPressed(java.awt.event.KeyEvent.VK_ALT)) { + // endWithoutTerminal(lastMouseCanvasPos, true); + //} + } + + @SGInit + public void initSG(G2DParentNode parent) { + ghostNode = parent.addNode(G2DParentNode.class); + ghostNode.setZIndex(PAINT_PRIORITY); + + ShapeNode pathNode = ghostNode.getOrCreateNode("path", ShapeNode.class); + pathNode.setColor(new Color(160, 0, 0)); + pathNode.setStroke(new BasicStroke(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 tis = pi.pickTerminals(me.controlPosition); + tis = TerminalUtil.findNearestOverlappingTerminals(tis); + if (!tis.isEmpty() && !containsStartTerminal(tis)) { + //System.out.println("end terminals (" + tis.size() + "):\n" + EString.implode(tis)); + for (TerminalInfo ti : tis) { + Pair canConnect = canConnect(ti.e, ti.t); + if (canConnect != null) { + connectionJudgment = canConnect.first; + + if (!isEndingInFlag() || !TerminalUtil.isSameTerminal(ti, endTerminal)) { + if (canConnect.second != null) { + controlPoints.getFirst() + .setPosition(canConnect.second.posDia) + .setAttachedToTerminal(canConnect.second); + } + controlPoints.getLast() + .setPosition(ti.posDia) + .setAttachedToTerminal(ti); + + selectedStartTerminal = canConnect.second; + endTerminal = ti; + } + + // Make sure that we are ending with a flag if ALT is pressed + // and no end terminal is defined. + 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 canConnect = canConnect(ti.e, ti.t); + if (canConnect != null) { + attachToConnectionJudgement = canConnect.first; + controlPoints.getLast().setPosition(ti.posDia).setAttachedToTerminal(ti); + endTerminal = ti; + startTerminal = canConnect.second; + 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 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 true 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 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 cps = new ArrayDeque<>(); + for (Iterator 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. + * + *

+ * At the same time it use {@link #forcedBranchPointDirection} to mark the + * current last branch point to be forcefully oriented according to the + * users wishes instead of calculating a default value for the orientation + * from the routed connection path. See + * {@link #calculateCurrentBranchPointDirection()} for more information on + * this. + * + *

+ * The logic of this method goes as follows: + *

    + *
  • Calculate the current branch point direction
  • + *
  • If the branch point direction is currently user selected ( + * {@link #forcedBranchPointDirection}
  • + *
  • + *
  • + *
+ * + * @param clockwise + * @return true if the rotation was successful + */ + protected boolean rotateLastBranchPoint(boolean clockwise) { + Direction oldDir = calculateCurrentBranchPointDirection(); + + if (forcedBranchPointDirection == null) { + forcedBranchPointDirection = oldDir.toggleDetermined(); + } else { + forcedBranchPointDirection = clockwise ? oldDir.cycleNext() : oldDir.cyclePrevious(); + } + + controlPoints.getLast().setDirection(forcedBranchPointDirection); + + updateSG(); + + return true; + } + + /** + * Set preferred direction for a branch/route point element. + * + * @param branchPoint the element to set the direction for + * @param direction the direction to set + * @return + */ + protected void setDirection(IElement branchPoint, Direction direction) { + branchPoint.getElementClass().getSingleItem(BranchPoint.class).setDirectionPreference(branchPoint, direction); + } + + protected Direction forcedBranchPointDirection() { + return forcedBranchPointDirection; + } + + protected void resetForcedBranchPointDirection() { + forcedBranchPointDirection = null; + } + + protected void forceBranchPointDirection(Direction direction) { + forcedBranchPointDirection = direction; + } + + /** + * @return + */ + protected Direction calculateCurrentBranchPointDirection() { + // If this is not the first branch point, toggle direction compared to + // last. + if (forcedBranchPointDirection != null) + return forcedBranchPointDirection; + + if (controlPoints.size() > 2) { + // This is not the first edge segment, toggle route point + // directions. + Iterator it = controlPoints.descendingIterator(); + it.next(); + ControlPoint secondLastCp = it.next(); + + Direction dir = secondLastCp.getDirection(); + switch (dir) { + case Horizontal: + return Direction.Vertical; + case Vertical: + return Direction.Horizontal; + case Any: + } + } + + // If this is the first branch point, calculate based on edge segment + // angle. + if (controlPoints.size() > 1) { + Iterator it = controlPoints.descendingIterator(); + ControlPoint last = it.next(); + ControlPoint secondLast = it.next(); + + double angle = Math.atan2(Math.abs(last.getPosition().getY() - secondLast.getPosition().getY()), + Math.abs(last.getPosition().getX() - secondLast.getPosition().getX())); + + if (angle >= 0 && angle < Math.PI / 4) { + return Direction.Horizontal; + } else if (angle > Math.PI / 4 && angle <= Math.PI / 2) { + return Direction.Vertical; + } + } + + return Direction.Any; + } + + protected boolean isEndingInFlag() { + return endFlag != null; + } + + /** + * @param mousePos + * @param altDown + * @return true if updateSG was executed, false + * 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 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 true if the specified element terminal matches any + * TerminalInfo in {@link #startTerminals} + */ + protected boolean isStartTerminal(IElement e, Terminal t) { + if (startTerminal == null) + return false; + for (TerminalInfo st : startTerminals) { + if (st.e == e && st.t == t) { + return true; + } + } + return false; + } + + /** + * @param e + * @param t + * @return true if the specified element terminal matches any + * TerminalInfo in {@link #startTerminals} + */ + protected boolean containsStartTerminal(List tis) { + if (startTerminal == null) + return false; + for (TerminalInfo st : startTerminals) { + for (TerminalInfo et : tis) { + if (st.e == et.e && st.t == et.t) { + return true; + } + } + } + return false; + } + + protected static FlagClass.Type endToFlagType(EdgeEnd end) { + switch (end) { + case Begin: + return FlagClass.Type.In; + case End: + return FlagClass.Type.Out; + default: + throw new IllegalArgumentException("unrecognized edge end: " + end); + } + } + + protected TerminalInfo createFlag(EdgeEnd connectionEnd) { + ElementClass flagClass = elementClassProvider.get(ElementClasses.FLAG); + IElement e = Element.spawnNew(flagClass); + + e.setHint(FlagClass.KEY_FLAG_TYPE, endToFlagType(connectionEnd)); + e.setHint(FlagClass.KEY_FLAG_MODE, FlagClass.Mode.Internal); + + TerminalInfo ti = new TerminalInfo(); + ti.e = e; + ti.t = ElementUtils.getSingleTerminal(e); + ti.posElem = TerminalUtil.getTerminalPosOnElement(e, ti.t); + ti.posDia = TerminalUtil.getTerminalPosOnDiagram(e, ti.t); + + return ti; + } + + protected boolean shouldEndWithFlag(MouseEvent me) { + return shouldEndWithFlag( me.hasAnyModifier(MouseEvent.ALT_MASK | MouseEvent.ALT_GRAPH_MASK) ); + } + + protected boolean shouldEndWithFlag(boolean altPressed) { + return altPressed && !isEndTerminalDefined() && createFlags && startFlag == null; + } + + protected boolean isEndTerminalDefined() { + return endTerminal != null; + } + + protected boolean isFlagTerminal(TerminalInfo ti) { + return ti.e.getElementClass().containsClass(FlagHandler.class); + } + + protected boolean allowReflexiveConnections() { + return false; + } + + protected boolean routePointsAllowed() { + return Boolean.TRUE.equals(diagram.getHint(DiagramHints.KEY_ALLOW_ROUTE_POINTS)); + } + + /** + * @param endElement + * @param endTerminal + * @return + */ + @SuppressWarnings("unchecked") + protected final Pair canConnect(IElement endElement, Terminal endTerminal) { + IConnectionAdvisor advisor = diagram.getHint(DiagramHints.CONNECTION_ADVISOR); + Object judgement = canConnect(advisor, endElement, endTerminal); + if (judgement == null) + return null; + if (judgement instanceof Pair) + return (Pair) judgement; + return Pair.make((ConnectionJudgement) judgement, startTerminal); + } + + protected Object canConnect(IConnectionAdvisor advisor, IElement endElement, Terminal endTerminal) { + if (advisor == null) + return Pair.make(ConnectionJudgement.CANBEMADELEGAL, startTerminal); + if (startTerminals.isEmpty()) { + ConnectionJudgement obj = (ConnectionJudgement) advisor.canBeConnected(null, null, null, endElement, endTerminal); + return obj != null ? Pair.make(obj, null) : null; + } + for (TerminalInfo st : startTerminals) { + ConnectionJudgement obj = (ConnectionJudgement) advisor.canBeConnected(null, st.e, st.t, endElement, endTerminal); + if (obj != null) { + return Pair.make(obj, st); + } + } + return null; + } + + /** + * For generating debugging information of what was attempted by the user + * when a connection couldn't be created. + * + * @param ts + * @return + */ + private String terminalsToString(final Iterable ts) { + try { + return Simantics.sync(new UniqueRead() { + @Override + public String perform(ReadGraph graph) throws DatabaseException { + DiagramResource DIA = DiagramResource.getInstance(graph); + ModelingResources MOD = ModelingResources.getInstance(graph); + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (TerminalInfo ti : ts) { + if (!first) + sb.append("\n"); + first = false; + sb.append("element "); + Object o = ElementUtils.getObject(ti.e); + if (o instanceof Resource) { + Resource er = (Resource) o; + Resource cer = graph.getPossibleObject(er, MOD.ElementToComponent); + Resource r = cer != null ? cer : er; + sb.append(NameUtils.getSafeName(graph, r)).append(" : "); + for (Resource type : graph.getPrincipalTypes(r)) { + sb.append(NameUtils.getSafeName(graph, type, true)); + } + } else { + sb.append(ti.e.toString()); + } + sb.append(", terminal "); + if (ti.t instanceof ResourceTerminal) { + Resource tr = ((ResourceTerminal) ti.t).getResource(); + Resource cp = graph.getPossibleObject(tr, DIA.HasConnectionPoint); + Resource r = cp != null ? cp : tr; + sb.append(NameUtils.getSafeName(graph, r, true)); + } else { + sb.append(ti.t.toString()); + } + } + return sb.toString(); + } + }); + } catch (DatabaseException e) { + return e.getMessage(); + } + } + } \ No newline at end of file