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%2FRouteGraphConnectTool.java;h=8f99207e19c77cb00f55288f8bd4d218aca06cdf;hp=57f5cd4900310263031a81d3227c99f355a25917;hb=13ff25bbcaaf3f36293a931403486ccc3897c9a6;hpb=28418dc0f3cc153ba631c201c900b99e45fa03d1 diff --git a/bundles/org.simantics.diagram/src/org/simantics/diagram/participant/RouteGraphConnectTool.java b/bundles/org.simantics.diagram/src/org/simantics/diagram/participant/RouteGraphConnectTool.java index 57f5cd490..8f99207e1 100644 --- a/bundles/org.simantics.diagram/src/org/simantics/diagram/participant/RouteGraphConnectTool.java +++ b/bundles/org.simantics.diagram/src/org/simantics/diagram/participant/RouteGraphConnectTool.java @@ -1,905 +1,924 @@ -/******************************************************************************* - * Copyright (c) 2007, 2016 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 - * Semantum Oy - Fixed bug #6364 - *******************************************************************************/ -package org.simantics.diagram.participant; - -import java.awt.AlphaComposite; -import java.awt.Composite; -import java.awt.Shape; -import java.awt.geom.AffineTransform; -import java.awt.geom.Point2D; -import java.awt.geom.Rectangle2D; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Deque; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; - -import org.simantics.Simantics; -import org.simantics.db.Resource; -import org.simantics.db.WriteGraph; -import org.simantics.db.common.request.WriteRequest; -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.RoutePoint; -import org.simantics.diagram.connection.RouteTerminal; -import org.simantics.diagram.connection.delta.RouteGraphDelta; -import org.simantics.diagram.connection.rendering.IRouteGraphRenderer; -import org.simantics.diagram.connection.rendering.arrows.ArrowLineEndStyle; -import org.simantics.diagram.connection.rendering.arrows.ILineEndStyle; -import org.simantics.diagram.connection.rendering.arrows.PlainLineEndStyle; -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.SGNodeReflection.SGCleanup; -import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit; -import org.simantics.g2d.connection.ConnectionEntity; -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.PickRequest; -import org.simantics.g2d.diagram.handler.Topology.Connection; -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.ElementHints; -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.FlagClass; -import org.simantics.g2d.elementclass.FlagHandler; -import org.simantics.g2d.participant.TransformUtil; -import org.simantics.g2d.utils.geom.DirectionSet; -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.ConnectionNode; -import org.simantics.scenegraph.g2d.nodes.connection.RouteGraphNode; -import org.simantics.scenegraph.g2d.snap.ISnapAdvisor; -import org.simantics.scenegraph.utils.GeometryUtils; -import org.simantics.structural2.modelingRules.ConnectionJudgement; -import org.simantics.utils.datastructures.Callback; -import org.simantics.utils.datastructures.Pair; -import org.simantics.utils.datastructures.Triple; -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 RouteGraphConnectTool extends AbstractMode { - - private static final String END_TERMINAL_DATA = "END"; - - public static final int PAINT_PRIORITY = ElementPainter.ELEMENT_PAINT_PRIORITY + 5; - - @Dependency - protected TransformUtil util; - - @Dependency - protected ElementPainter diagramPainter; - - @Dependency - protected PointerInteractor pi; - - /** - * Starting point designation. - * - * The value is received by the constructor. - */ - protected RouteGraphTarget startingPoint; - - protected TerminalInfo startTerminal = new TerminalInfo(); - - /** - * true if this tool should create connection continuation - * flags, false otherwise. - */ - protected boolean createFlags; - - /** - * - */ - protected IElementClassProvider elementClassProvider; - - /** - * - */ - protected Deque controlPoints = new ArrayDeque(); - - /** - * 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 #startTerminal} and {@link #endTerminal}. - */ - protected ConnectionJudgement connectionJudgment; - - /** - * 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) { - 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 ConnectionNode ghostNode; - - protected RouteGraphNode rgNode; - - protected RouteGraph routeGraph; - protected RouteTerminal endRouteTerminal; - private ILineEndStyle endTerminalStyle; - - /** - * Indicates whether the connection is about to be ended into a new - * flag/branchpoint or not. - */ - protected TerminalInfo endFlag; - - protected G2DParentNode endFlagNode; - - private RouteLine attachedToRouteLine; - - protected RouteGraph beforeRouteGraph; - - private IRouteGraphRenderer beforeRenderer; - - private boolean beforeEditable; - - protected RouteGraphDelta routeGraphDelta; - - private Set> alreadyConnected; - - /** - * @param startElement - * @param routeGraphConnection - * @param mouseId - * @param startCanvasPos - */ - public RouteGraphConnectTool(RouteGraphTarget startingPoint, int mouseId) { - super(mouseId); - - Point2D intersection = startingPoint.getIntersectionPosition(); - - this.startingPoint = startingPoint; - this.lastMouseCanvasPos.setLocation(intersection); - - BranchPointTerminal t = BranchPointTerminal.existingTerminal( - AffineTransform.getTranslateInstance(intersection.getX(), intersection.getY()), - DirectionSet.ANY, - BranchPointNode.SHAPE); - - Point2D p = startingPoint.getIntersectionPosition(); - AffineTransform at = AffineTransform.getTranslateInstance(p.getX(), p.getY()); - - startTerminal.e = startingPoint.getElement(); - startTerminal.t = t; - startTerminal.posElem = at; - startTerminal.posDia = at; - startTerminal.shape = t.getShape(); - - controlPoints.add( newControlPoint( startingPoint.getIntersectionPosition() ) ); - controlPoints.add( newControlPoint( startingPoint.getCanvasPosition() ) ); - - alreadyConnected = new HashSet>(); - IElement connection = startingPoint.getElement(); - ConnectionEntity ce = connection.getHint(ElementHints.KEY_CONNECTION_ENTITY); - Collection tcs = ce.getTerminalConnections(null); - for (Connection tc : tcs) - alreadyConnected.add(Pair.make(tc.node, tc.terminal)); - } - - @Override - public void addedToContext(ICanvasContext ctx) { - super.addedToContext(ctx); - - // 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)); - } - } - - @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); - } - - super.removedFromContext(ctx); - } - - int straightDirections(RouteLine line) { - return line.isHorizontal() - ? (RouteTerminal.DIR_DOWN | RouteTerminal.DIR_UP) - : (RouteTerminal.DIR_LEFT | RouteTerminal.DIR_RIGHT); - } - - private static RouteTerminal addTerminal(Object data, RouteGraph rg, double x, double y, Rectangle2D bounds, int allowedDirections, ILineEndStyle style) { - RouteTerminal rt; - if (bounds != null) - rt = rg.addTerminal(x, y, bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMaxY(), allowedDirections, style); - else - rt = rg.addTerminal(x, y, x, y, x, y, allowedDirections, style); - rt.setData( data ); - return rt; - } - - private RouteTerminal setEndTerminal(double x, double y, Rectangle2D bounds, int allowedDirections) { - - // First add then remove to prevent deletion of route lines in case there are 2 only terminals - - RouteTerminal toRemove = endRouteTerminal; - - endRouteTerminal = addTerminal( END_TERMINAL_DATA, routeGraph, x, y, bounds, allowedDirections, endTerminalStyle ); - routeGraph.link( attachedToRouteLine, endRouteTerminal ); - - if (toRemove != null) - routeGraph.remove(toRemove); - - return endRouteTerminal; - - } - - protected void setEndTerminal(TerminalInfo ti) { - Rectangle2D bounds = ElementUtils.getElementBoundsOnDiagram(ti.e, new Rectangle2D.Double()); - GeometryUtils.expandRectangle(bounds, 2); - int dir = RouteGraphConnectionClass.shortestDirectionOutOfBounds( - ti.posDia.getTranslateX(), ti.posDia.getTranslateY(), bounds); - setEndTerminal(ti.posDia.getTranslateX(), ti.posDia.getTranslateY(), bounds, dir); - } - - @SGInit - public void initSG(G2DParentNode parent) { - ghostNode = parent.addNode("branched connection", ConnectionNode.class); - //ghostNode.setComposite(AlphaComposite.SrcOver.derive(0.5f)); - ghostNode.setZIndex(PAINT_PRIORITY); - - rgNode = ghostNode.addNode("branch", RouteGraphNode.class); - - double ex = startingPoint.getCanvasPosition().getX(); - double ey = startingPoint.getCanvasPosition().getY(); - - beforeRouteGraph = startingPoint.getNode().getRouteGraph(); - beforeEditable = startingPoint.getNode().isEditable(); - - RouteGraphNode beforeRgNode = startingPoint.getNode(); - beforeRenderer = beforeRgNode.getRenderer(); - - rgNode.setRenderer(beforeRenderer); - rgNode.setEditable(false); - - beforeRgNode.setEditable(beforeEditable); - beforeRgNode.setRenderer(null); - - initRG(ex, ey); - - } - - public void initRG(double ex, double ey) { - - THashMap map = new THashMap(); - routeGraph = beforeRouteGraph.copy(map); - - endTerminalStyle = PlainLineEndStyle.INSTANCE; - for (RouteTerminal t : routeGraph.getTerminals()) { - if (t.getRenderStyle() instanceof ArrowLineEndStyle) { - endTerminalStyle = t.getRenderStyle(); - break; - } - } - - attachedToRouteLine = (RouteLine) map.get(startingPoint.getLine()); - routeGraph.makePersistent(attachedToRouteLine); - for (RouteLine line : routeGraph.getAllLines()) { - if (!line.isTransient() && line.isHorizontal() == attachedToRouteLine.isHorizontal() - && line.getPosition() == attachedToRouteLine.getPosition()) { - attachedToRouteLine = line; - break; - } - } - routeGraphDelta = new RouteGraphDelta(beforeRouteGraph, routeGraph); - -// beforeRouteGraph.print(); -// routeGraph.print(); -// routeGraphDelta.print(); - - setEndTerminal(ex, ey, null, 0xf); - - rgNode.setRouteGraph(routeGraph); - - } - - @SGCleanup - public void cleanupSG() { - RouteGraphNode beforeRgNode = startingPoint.getNode(); - beforeRgNode.setRouteGraph(beforeRouteGraph); - beforeRgNode.setRenderer(beforeRenderer); - beforeRgNode.setEditable(beforeEditable); - - ghostNode.remove(); - ghostNode = null; - - setDirty(); - } - - @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)) { - // TODO: rotate flag? - } - 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())); - } - - TerminalInfo ti = pi.pickTerminal(me.controlPosition); - if (ti != null) { - Object canConnect = canConnect(ti.e, ti.t); - if (canConnect != null) { - connectionJudgment = (ConnectionJudgement) canConnect; - - if (!isEndingInFlag() || !TerminalUtil.isSameTerminal(ti, endTerminal)) { - controlPoints.getLast() - .setPosition(ti.posDia) - .setAttachedToTerminal(ti); - - endTerminal = ti; - - connect(ti); - - } - - // Make sure that we are ending with a flag if ALT is pressed - // and no end terminal is defined. - endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me)); - - updateSG(new Point2D.Double(ti.posDia.getTranslateX(), ti.posDia.getTranslateY())); - return false; - } - } - - connectionJudgment = null; - if (isEndTerminalDefined()) { - // CASE: Mouse was previously on top of a valid terminal to end - // the connection. Now the mouse has been moved where there is - // no longer a terminal to connect to. - // - // => Disconnect the last edge segment from the previous - // terminal, mark endElement/endTerminal non-existent - // and connect the disconnected edge to a new branch point. - - disconnect(mouseCanvasPos); - - controlPoints.getLast() - .setPosition(mouseCanvasPos) - .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); - } - - // Make sure that we are ending with a flag if ALT is pressed and no end - // terminal is defined. - endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me)); - - updateSG(lastMouseCanvasPos); - - return false; - } - - protected void connect(TerminalInfo ti) { - setEndTerminal(ti); - } - - protected void disconnect(Point2D mouseCanvasPos) { - setEndTerminal(mouseCanvasPos.getX(), mouseCanvasPos.getY(), null, 0xf); - } - - protected boolean processMouseButtonPress(MouseButtonPressedEvent e) { - MouseButtonEvent me = e; - - // Do nothing before the mouse has moved at least a little. - // This prevents the user from ending the connection right where - // it started. - if (!mouseHasMoved) - return true; - - if (me.button == MouseEvent.LEFT_BUTTON) { - Point2D mouseControlPos = me.controlPosition; - Point2D mouseCanvasPos = util.getInverseTransform().transform(mouseControlPos, new Point2D.Double()); - - ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR); - if (snapAdvisor != null) - snapAdvisor.snap(mouseCanvasPos); - - // Clicked on an allowed end terminal. End connection & end mode. - if (isEndTerminalDefined()) { - createConnection(); - remove(); - return true; - } else { - // Finish connection in thin air only if the - // connection was started from a valid terminal. - if ((me.stateMask & MouseEvent.ALT_MASK) != 0 && startTerminal != null) { - connectionJudgment = (ConnectionJudgement) canConnect(null, null); - if (connectionJudgment == null) - return true; - createConnection(); - remove(); - return true; - } else if (routePointsAllowed() - && (me.stateMask & (MouseEvent.ALT_MASK | MouseEvent.SHIFT_MASK | MouseEvent.CTRL_MASK)) == 0) { - // Add new connection control point. - controlPoints.add(newControlPoint(mouseCanvasPos)); - updateSG(mouseCanvasPos); - } - } - - // Eat the event to prevent other participants from doing - // incompatible things while in this connection mode. - return true; - } else if (me.button == MouseEvent.RIGHT_BUTTON) { - return cancelPreviousBend(); - } - - return false; - } - - protected boolean cancelPreviousBend() { - if (!routePointsAllowed()) - return false; - - // Just to make this code more comprehensible, prevent an editing - // case that requires ugly code to work. - if (isEndingInFlag()) - return true; - - // If there are no real route points, cancel whole connection. - if (controlPoints.size() <= 2) { - setDirty(); - remove(); - return true; - } - - // Cancel last bend - controlPoints.removeLast(); - controlPoints.getLast().setPosition(lastMouseCanvasPos); - - updateSG(lastMouseCanvasPos); - return true; - } - - protected void updateSG(Point2D mousePos) { - routeGraph.setLocation(endRouteTerminal, mousePos.getX(), mousePos.getY()); - //routeGraph.print(System.err); - setDirty(); - } - - protected boolean shouldEndWithFlag(MouseEvent me) { - return shouldEndWithFlag((me.stateMask & MouseEvent.ALT_MASK) != 0); - } - - protected boolean shouldEndWithFlag(boolean altPressed) { - return altPressed && !isEndTerminalDefined() && createFlags; - } - - protected boolean isEndTerminalDefined() { - return endTerminal != null; - } - - protected boolean isFlagTerminal(TerminalInfo ti) { - return ti.e.getElementClass().containsClass(FlagHandler.class); - } - - - protected boolean isEndingInFlag() { - return endFlag != null; - } - - protected void endWithoutTerminal(Point2D mousePos, boolean altDown) { - // Just go with branch points if flags are not allowed. - if (!createFlags) - return; - - boolean endTerminalDefined = isEndTerminalDefined(); - - if (altDown) { - if (!isEndingInFlag()) { - endFlag = createFlag(EdgeEnd.End); - endFlagNode = showElement(ghostNode, "endFlag", endFlag.e, mousePos); - controlPoints.getLast() - .setAttachedToTerminal(endFlag); - - // TerminalPainter must refresh - setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy); - - updateSG(mousePos); - } - } 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.setAttachedToTerminal(endTerminal); - - if (endTerminalDefined) { - cp.setPosition(endTerminal.posDia); - } else { - cp.setPosition(mousePos); - } - - // Force Terminalpainter refresh - setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy); - - updateSG(mousePos); - } - } - } - - 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; - } - - /** - * @param canvasPos - * @return - */ - protected ControlPoint newControlPoint(Point2D canvasPos) { - return new ControlPoint(canvasPos); - } - - protected Triple prepareRouteGraphDelta() { - // Prevent route graph connection synchronization from crashing on the - // transient route terminal that doesn't exist yet. It is created after - // persisting the route line, which is the only purpose of this route - // graph synchronization. - routeGraph.remove(endRouteTerminal); - return Triple.make(beforeRouteGraph, routeGraph, routeGraphDelta); - } - - protected void createConnection() { - TimeLogger.resetTimeAndLog(getClass(), "createConnection"); - final ConnectionJudgement judgment = this.connectionJudgment; - if (judgment == null) { - ErrorLogger.defaultLogError("Cannot create connection, no judgment available on connection validity", null); - return; - } - - final ConnectionBuilder builder = new ConnectionBuilder(this.diagram); - final Triple rgs = prepareRouteGraphDelta(); - - Simantics.getSession().asyncRequest(new WriteRequest() { - @Override - public void perform(WriteGraph graph) throws DatabaseException { - graph.markUndoPoint(); - Resource connection = ElementUtils.getObject(startTerminal.e); - if (!rgs.third.isEmpty()) { - new RouteGraphConnection(graph, connection).synchronize(graph, rgs.first, rgs.second, rgs.third); - } - Resource attachToLine = RouteGraphConnection.deserialize(graph, attachedToRouteLine.getData()); - builder.attachToRouteGraph(graph, judgment, connection, attachToLine, controlPoints, endTerminal, FlagClass.Type.Out); - } - }, new Callback() { - @Override - public void run(DatabaseException parameter) { - if (parameter != null) - ExceptionUtils.logAndShowError(parameter); - } - }); - } - - protected boolean routePointsAllowed() { - return Boolean.TRUE.equals(diagram.getHint(DiagramHints.KEY_ALLOW_ROUTE_POINTS)); - } - - protected Object canConnect(IElement endElement, Terminal endTerminal) { - IConnectionAdvisor advisor = diagram.getHint(DiagramHints.CONNECTION_ADVISOR); - return canConnect(advisor, endElement, endTerminal); - } - - protected Object canConnect(IConnectionAdvisor advisor, IElement endElement, Terminal endTerminal) { - if (advisor == null) - return ConnectionJudgement.CANBEMADELEGAL; - if (alreadyConnected.contains(Pair.make(endElement, endTerminal))) - return null; - return advisor.canBeConnected(null, startTerminal.e, startTerminal.t, endElement, endTerminal); - } - - 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; - } - - // ------------------------------------------------------------------------ - - static RouteGraphTarget pickRouteGraphConnection(IDiagram diagram, Shape pickShape, double pickDistance) { - ArrayList elements = new ArrayList(); - PickRequest req = new PickRequest(pickShape); - DiagramUtils.pick(diagram, req, elements); - for (Iterator it = elements.iterator(); it.hasNext();) { - IElement e = it.next(); - RouteGraphNode rgn = e.getHint(RouteGraphConnectionClass.KEY_RG_NODE); - if (rgn == null || rgn.getRouteGraph() == null) - it.remove(); - } - if (elements.isEmpty()) - return null; - - Rectangle2D pickRect = pickShape.getBounds2D(); - final double x = pickRect.getCenterX(); - final double y = pickRect.getCenterY(); - - return pickNearestRouteGraphConnection(elements, x, y, pickDistance); - } - - private static RouteGraphTarget pickNearestRouteGraphConnection(ArrayList elements, double x, double y, double pd) { - // Find the nearest distance at which we get hits. - double hi = pd + 1; - double lo = hi * .01; - double limit = 0.5; - while (true) { - double delta = (hi - lo); - if (delta <= limit) - break; - - pd = (lo + hi) * .5; - - boolean hit = false; - for (IElement connection : elements) { - RouteGraphNode rgn = connection.getHint(RouteGraphConnectionClass.KEY_RG_NODE); - RouteLine line = rgn.getRouteGraph().pickLine(x, y, pd); - if (line != null) { - hit = true; - break; - } - } - - if (hit) - hi = pd; - else - lo = pd; - } - - // Now that the nearest hitting distance is found, find the nearest intersection. - RouteGraphTarget nearestTarget = null; - double nearest = Double.MAX_VALUE; - for (IElement connection : elements) { - RouteGraphNode rgn = connection.getHint(RouteGraphConnectionClass.KEY_RG_NODE); - RouteLine line = rgn.getRouteGraph().pickLine(x, y, pd); - if (line == null) - continue; - - Point2D intersection = intersectionPoint(x, y, line); - if (intersection == null) - continue; - - double dx = intersection.getX() - x; - double dy = intersection.getY() - y; - double dist = Math.sqrt(dx*dx + dy*dy); - if (dist < nearest) { - nearest = dist; - nearestTarget = new RouteGraphTarget(connection, rgn, line, new Point2D.Double(x, y), intersection); - } - } - - return nearestTarget; - } - - static Point2D intersectionPoint(double x, double y, RouteLine line) { - Collection points = line.getPoints(); - if (points.size() < 2) - return null; - RoutePoint s = line.getBegin(); - RoutePoint e = line.getEnd(); - return GeometryUtils.intersectionToLine(s.getX(), s.getY(), e.getX(), e.getY(), x, y); - } - +/******************************************************************************* + * Copyright (c) 2007, 2016 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 + * Semantum Oy - Fixed bug #6364 + *******************************************************************************/ +package org.simantics.diagram.participant; + +import java.awt.AlphaComposite; +import java.awt.Composite; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Deque; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.simantics.Simantics; +import org.simantics.db.Resource; +import org.simantics.db.WriteGraph; +import org.simantics.db.common.request.WriteRequest; +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.RoutePoint; +import org.simantics.diagram.connection.RouteTerminal; +import org.simantics.diagram.connection.delta.RouteGraphDelta; +import org.simantics.diagram.connection.rendering.IRouteGraphRenderer; +import org.simantics.diagram.connection.rendering.arrows.ArrowLineEndStyle; +import org.simantics.diagram.connection.rendering.arrows.ILineEndStyle; +import org.simantics.diagram.connection.rendering.arrows.PlainLineEndStyle; +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.SGNodeReflection.SGCleanup; +import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit; +import org.simantics.g2d.connection.ConnectionEntity; +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.PickRequest; +import org.simantics.g2d.diagram.handler.Topology.Connection; +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.ElementHints; +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.FlagClass; +import org.simantics.g2d.elementclass.FlagHandler; +import org.simantics.g2d.participant.TransformUtil; +import org.simantics.g2d.utils.geom.DirectionSet; +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.ConnectionNode; +import org.simantics.scenegraph.g2d.nodes.connection.RouteGraphNode; +import org.simantics.scenegraph.g2d.snap.ISnapAdvisor; +import org.simantics.scenegraph.utils.GeometryUtils; +import org.simantics.structural2.modelingRules.ConnectionJudgement; +import org.simantics.utils.datastructures.Pair; +import org.simantics.utils.datastructures.Triple; +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 RouteGraphConnectTool extends AbstractMode { + + private static final String END_TERMINAL_DATA = "END"; + + public static final int PAINT_PRIORITY = ElementPainter.ELEMENT_PAINT_PRIORITY + 5; + + @Dependency + protected TransformUtil util; + + @Dependency + protected ElementPainter diagramPainter; + + @Dependency + protected PointerInteractor pi; + + /** + * Starting point designation. + * + * The value is received by the constructor. + */ + protected RouteGraphTarget startingPoint; + + protected TerminalInfo startTerminal = new TerminalInfo(); + + /** + * true if this tool should create connection continuation + * flags, false otherwise. + */ + protected boolean createFlags; + + /** + * + */ + protected IElementClassProvider elementClassProvider; + + /** + * + */ + protected Deque controlPoints = new ArrayDeque(); + + /** + * 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 #startTerminal} and {@link #endTerminal}. + */ + protected ConnectionJudgement connectionJudgment; + + /** + * 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) { + 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 ConnectionNode ghostNode; + + protected RouteGraphNode rgNode; + + protected RouteGraph routeGraph; + protected RouteTerminal endRouteTerminal; + private ILineEndStyle endTerminalStyle; + + /** + * Indicates whether the connection is about to be ended into a new + * flag/branchpoint or not. + */ + protected TerminalInfo endFlag; + + protected G2DParentNode endFlagNode; + + private RouteLine attachedToRouteLine; + + protected RouteGraph beforeRouteGraph; + + private IRouteGraphRenderer beforeRenderer; + + private boolean beforeEditable; + + protected RouteGraphDelta routeGraphDelta; + + private Set> alreadyConnected; + + /** + * @param startElement + * @param routeGraphConnection + * @param mouseId + * @param startCanvasPos + */ + public RouteGraphConnectTool(RouteGraphTarget startingPoint, int mouseId) { + super(mouseId); + + Point2D intersection = startingPoint.getIntersectionPosition(); + + this.startingPoint = startingPoint; + this.lastMouseCanvasPos.setLocation(intersection); + + BranchPointTerminal t = BranchPointTerminal.existingTerminal( + AffineTransform.getTranslateInstance(intersection.getX(), intersection.getY()), + DirectionSet.ANY, + BranchPointNode.SHAPE); + + Point2D p = startingPoint.getIntersectionPosition(); + AffineTransform at = AffineTransform.getTranslateInstance(p.getX(), p.getY()); + + startTerminal.e = startingPoint.getElement(); + startTerminal.t = t; + startTerminal.posElem = at; + startTerminal.posDia = at; + startTerminal.shape = t.getShape(); + + controlPoints.add( newControlPoint( startingPoint.getIntersectionPosition() ) ); + controlPoints.add( newControlPoint( startingPoint.getCanvasPosition() ) ); + + alreadyConnected = new HashSet>(); + IElement connection = startingPoint.getElement(); + ConnectionEntity ce = connection.getHint(ElementHints.KEY_CONNECTION_ENTITY); + Collection tcs = ce.getTerminalConnections(null); + for (Connection tc : tcs) + alreadyConnected.add(Pair.make(tc.node, tc.terminal)); + } + + @Override + public void addedToContext(ICanvasContext ctx) { + super.addedToContext(ctx); + + // 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)); + } + } + + @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); + } + + super.removedFromContext(ctx); + } + + int straightDirections(RouteLine line) { + return line.isHorizontal() + ? (RouteTerminal.DIR_DOWN | RouteTerminal.DIR_UP) + : (RouteTerminal.DIR_LEFT | RouteTerminal.DIR_RIGHT); + } + + private static RouteTerminal addTerminal(Object data, RouteGraph rg, double x, double y, Rectangle2D bounds, int allowedDirections, ILineEndStyle style) { + RouteTerminal rt; + if (bounds != null) + rt = rg.addTerminal(x, y, bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMaxY(), allowedDirections, style); + else + rt = rg.addTerminal(x, y, x, y, x, y, allowedDirections, style); + rt.setData( data ); + return rt; + } + + private RouteTerminal setEndTerminal(double x, double y, Rectangle2D bounds, int allowedDirections) { + + // First add then remove to prevent deletion of route lines in case there are 2 only terminals + + RouteTerminal toRemove = endRouteTerminal; + + endRouteTerminal = addTerminal( END_TERMINAL_DATA, routeGraph, x, y, bounds, allowedDirections, endTerminalStyle ); + routeGraph.link( attachedToRouteLine, endRouteTerminal ); + + if (toRemove != null) + routeGraph.remove(toRemove); + + return endRouteTerminal; + + } + + protected void setEndTerminal(TerminalInfo ti) { + Rectangle2D bounds = ElementUtils.getElementBoundsOnDiagram(ti.e, new Rectangle2D.Double()); + GeometryUtils.expandRectangle(bounds, 2); + int dir = RouteGraphConnectionClass.shortestDirectionOutOfBounds( + ti.posDia.getTranslateX(), ti.posDia.getTranslateY(), bounds); + setEndTerminal(ti.posDia.getTranslateX(), ti.posDia.getTranslateY(), bounds, dir); + } + + @SGInit + public void initSG(G2DParentNode parent) { + ghostNode = parent.addNode("branched connection", ConnectionNode.class); + //ghostNode.setComposite(AlphaComposite.SrcOver.derive(0.5f)); + ghostNode.setZIndex(PAINT_PRIORITY); + + rgNode = ghostNode.addNode("branch", RouteGraphNode.class); + + double ex = startingPoint.getCanvasPosition().getX(); + double ey = startingPoint.getCanvasPosition().getY(); + + beforeRouteGraph = startingPoint.getNode().getRouteGraph(); + beforeEditable = startingPoint.getNode().isEditable(); + + RouteGraphNode beforeRgNode = startingPoint.getNode(); + beforeRenderer = beforeRgNode.getRenderer(); + + rgNode.setRenderer(beforeRenderer); + rgNode.setEditable(false); + + beforeRgNode.setEditable(beforeEditable); + beforeRgNode.setRenderer(null); + + initRG(ex, ey); + + } + + public void initRG(double ex, double ey) { + + THashMap map = new THashMap(); + routeGraph = beforeRouteGraph.copy(map); + + endTerminalStyle = PlainLineEndStyle.INSTANCE; + for (RouteTerminal t : routeGraph.getTerminals()) { + if (t.getRenderStyle() instanceof ArrowLineEndStyle) { + endTerminalStyle = t.getRenderStyle(); + break; + } + } + + attachedToRouteLine = (RouteLine) map.get(startingPoint.getLine()); + routeGraph.makePersistent(attachedToRouteLine); + for (RouteLine line : routeGraph.getAllLines()) { + if (!line.isTransient() && line.isHorizontal() == attachedToRouteLine.isHorizontal() + && line.getPosition() == attachedToRouteLine.getPosition()) { + attachedToRouteLine = line; + break; + } + } + routeGraphDelta = new RouteGraphDelta(beforeRouteGraph, routeGraph); + +// beforeRouteGraph.print(); +// routeGraph.print(); +// routeGraphDelta.print(); + + setEndTerminal(ex, ey, null, 0xf); + + rgNode.setRouteGraph(routeGraph); + + } + + @SGCleanup + public void cleanupSG() { + RouteGraphNode beforeRgNode = startingPoint.getNode(); + beforeRgNode.setRouteGraph(beforeRouteGraph); + beforeRgNode.setRenderer(beforeRenderer); + beforeRgNode.setEditable(beforeEditable); + + ghostNode.remove(); + ghostNode = null; + + setDirty(); + } + + @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)) { + // TODO: rotate flag? + } + 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())); + } + + TerminalInfo ti = pi.pickTerminal(me.controlPosition); + if (ti != null) { + Object canConnect = canConnect(ti.e, ti.t); + if (canConnect != null) { + connectionJudgment = (ConnectionJudgement) canConnect; + + if (!isEndingInFlag() || !TerminalUtil.isSameTerminal(ti, endTerminal)) { + controlPoints.getLast() + .setPosition(ti.posDia) + .setAttachedToTerminal(ti); + + endTerminal = ti; + + connect(ti); + + } + + // Make sure that we are ending with a flag if ALT is pressed + // and no end terminal is defined. + endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me)); + + updateSG(new Point2D.Double(ti.posDia.getTranslateX(), ti.posDia.getTranslateY())); + return false; + } + } + + connectionJudgment = null; + if (isEndTerminalDefined()) { + // CASE: Mouse was previously on top of a valid terminal to end + // the connection. Now the mouse has been moved where there is + // no longer a terminal to connect to. + // + // => Disconnect the last edge segment from the previous + // terminal, mark endElement/endTerminal non-existent + // and connect the disconnected edge to a new branch point. + + disconnect(mouseCanvasPos); + + controlPoints.getLast() + .setPosition(mouseCanvasPos) + .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); + } + + // Make sure that we are ending with a flag if ALT is pressed and no end + // terminal is defined. + endWithoutTerminal(lastMouseCanvasPos, shouldEndWithFlag(me)); + + updateSG(lastMouseCanvasPos); + + return false; + } + + protected void connect(TerminalInfo ti) { + setEndTerminal(ti); + } + + protected void disconnect(Point2D mouseCanvasPos) { + setEndTerminal(mouseCanvasPos.getX(), mouseCanvasPos.getY(), null, 0xf); + } + + protected boolean processMouseButtonPress(MouseButtonEvent me) { + // Do nothing before the mouse has moved at least a little. + // This prevents the user from ending the connection right where + // it started. + if (!mouseHasMoved) + return true; + + if (me.button == MouseEvent.LEFT_BUTTON) { + Point2D mouseControlPos = me.controlPosition; + Point2D mouseCanvasPos = util.getInverseTransform().transform(mouseControlPos, new Point2D.Double()); + + ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR); + if (snapAdvisor != null) + snapAdvisor.snap(mouseCanvasPos); + + // Clicked on an allowed end terminal. End connection & end mode. + if (tryEndConnection()) { + return true; + } else { + // Finish connection in thin air only if the + // connection was started from a valid terminal. + if ((me.stateMask & MouseEvent.ALT_MASK) != 0 && startTerminal != null) { + connectionJudgment = (ConnectionJudgement) canConnect(null, null); + if (connectionJudgment == null) + return true; + createConnection(); + remove(); + return true; + } else if (routePointsAllowed() + && (me.stateMask & (MouseEvent.ALT_MASK | MouseEvent.SHIFT_MASK | MouseEvent.CTRL_MASK)) == 0) { + // Add new connection control point. + controlPoints.add(newControlPoint(mouseCanvasPos)); + updateSG(mouseCanvasPos); + } + } + + // 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()) { + createConnection(); + remove(); + return true; + } + return false; + } + + protected boolean cancelPreviousBend() { + if (!routePointsAllowed()) + return false; + + // Just to make this code more comprehensible, prevent an editing + // case that requires ugly code to work. + if (isEndingInFlag()) + return true; + + // If there are no real route points, cancel whole connection. + if (controlPoints.size() <= 2) { + setDirty(); + remove(); + return true; + } + + // Cancel last bend + controlPoints.removeLast(); + controlPoints.getLast().setPosition(lastMouseCanvasPos); + + updateSG(lastMouseCanvasPos); + return true; + } + + protected void updateSG(Point2D mousePos) { + routeGraph.setLocation(endRouteTerminal, mousePos.getX(), mousePos.getY()); + //routeGraph.print(System.err); + setDirty(); + } + + protected boolean shouldEndWithFlag(MouseEvent me) { + return shouldEndWithFlag((me.stateMask & MouseEvent.ALT_MASK) != 0); + } + + protected boolean shouldEndWithFlag(boolean altPressed) { + return altPressed && !isEndTerminalDefined() && createFlags; + } + + protected boolean isEndTerminalDefined() { + return endTerminal != null; + } + + protected boolean isFlagTerminal(TerminalInfo ti) { + return ti.e.getElementClass().containsClass(FlagHandler.class); + } + + + protected boolean isEndingInFlag() { + return endFlag != null; + } + + protected void endWithoutTerminal(Point2D mousePos, boolean altDown) { + // Just go with branch points if flags are not allowed. + if (!createFlags) + return; + + boolean endTerminalDefined = isEndTerminalDefined(); + + if (altDown) { + if (!isEndingInFlag()) { + endFlag = createFlag(EdgeEnd.End); + endFlagNode = showElement(ghostNode, "endFlag", endFlag.e, mousePos); + controlPoints.getLast() + .setAttachedToTerminal(endFlag); + + // TerminalPainter must refresh + setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy); + + updateSG(mousePos); + } + } 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.setAttachedToTerminal(endTerminal); + + if (endTerminalDefined) { + cp.setPosition(endTerminal.posDia); + } else { + cp.setPosition(mousePos); + } + + // Force Terminalpainter refresh + setHint(TerminalPainter.TERMINAL_HOVER_STRATEGY, terminalHoverStrategy); + + updateSG(mousePos); + } + } + } + + 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; + } + + /** + * @param canvasPos + * @return + */ + protected ControlPoint newControlPoint(Point2D canvasPos) { + return new ControlPoint(canvasPos); + } + + protected Triple prepareRouteGraphDelta() { + // Prevent route graph connection synchronization from crashing on the + // transient route terminal that doesn't exist yet. It is created after + // persisting the route line, which is the only purpose of this route + // graph synchronization. + routeGraph.remove(endRouteTerminal); + return Triple.make(beforeRouteGraph, routeGraph, routeGraphDelta); + } + + protected void createConnection() { + TimeLogger.resetTimeAndLog(getClass(), "createConnection"); + final ConnectionJudgement judgment = this.connectionJudgment; + if (judgment == null) { + ErrorLogger.defaultLogError("Cannot create connection, no judgment available on connection validity", null); + return; + } + + final ConnectionBuilder builder = new ConnectionBuilder(this.diagram); + final Triple rgs = prepareRouteGraphDelta(); + + Simantics.getSession().asyncRequest(new WriteRequest() { + @Override + public void perform(WriteGraph graph) throws DatabaseException { + graph.markUndoPoint(); + Resource connection = ElementUtils.getObject(startTerminal.e); + if (!rgs.third.isEmpty()) { + new RouteGraphConnection(graph, connection).synchronize(graph, rgs.first, rgs.second, rgs.third); + } + Resource attachToLine = RouteGraphConnection.deserialize(graph, attachedToRouteLine.getData()); + builder.attachToRouteGraph(graph, judgment, connection, attachToLine, controlPoints, endTerminal, FlagClass.Type.Out); + } + }, e -> { + if (e != null) + ExceptionUtils.logAndShowError(e); + }); + } + + protected boolean routePointsAllowed() { + return Boolean.TRUE.equals(diagram.getHint(DiagramHints.KEY_ALLOW_ROUTE_POINTS)); + } + + protected Object canConnect(IElement endElement, Terminal endTerminal) { + IConnectionAdvisor advisor = diagram.getHint(DiagramHints.CONNECTION_ADVISOR); + return canConnect(advisor, endElement, endTerminal); + } + + protected Object canConnect(IConnectionAdvisor advisor, IElement endElement, Terminal endTerminal) { + if (advisor == null) + return ConnectionJudgement.CANBEMADELEGAL; + if (alreadyConnected.contains(Pair.make(endElement, endTerminal))) + return null; + return advisor.canBeConnected(null, startTerminal.e, startTerminal.t, endElement, endTerminal); + } + + 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; + } + + // ------------------------------------------------------------------------ + + static RouteGraphTarget pickRouteGraphConnection(IDiagram diagram, Shape pickShape, double pickDistance) { + ArrayList elements = new ArrayList(); + PickRequest req = new PickRequest(pickShape); + DiagramUtils.pick(diagram, req, elements); + for (Iterator it = elements.iterator(); it.hasNext();) { + IElement e = it.next(); + RouteGraphNode rgn = e.getHint(RouteGraphConnectionClass.KEY_RG_NODE); + if (rgn == null || rgn.getRouteGraph() == null) + it.remove(); + } + if (elements.isEmpty()) + return null; + + Rectangle2D pickRect = pickShape.getBounds2D(); + final double x = pickRect.getCenterX(); + final double y = pickRect.getCenterY(); + + return pickNearestRouteGraphConnection(elements, x, y, pickDistance); + } + + private static RouteGraphTarget pickNearestRouteGraphConnection(ArrayList elements, double x, double y, double pd) { + // Find the nearest distance at which we get hits. + double hi = pd + 1; + double lo = hi * .01; + double limit = 0.5; + while (true) { + double delta = (hi - lo); + if (delta <= limit) + break; + + pd = (lo + hi) * .5; + + boolean hit = false; + for (IElement connection : elements) { + RouteGraphNode rgn = connection.getHint(RouteGraphConnectionClass.KEY_RG_NODE); + RouteLine line = rgn.getRouteGraph().pickLine(x, y, pd); + if (line != null) { + hit = true; + break; + } + } + + if (hit) + hi = pd; + else + lo = pd; + } + + // Now that the nearest hitting distance is found, find the nearest intersection. + RouteGraphTarget nearestTarget = null; + double nearest = Double.MAX_VALUE; + for (IElement connection : elements) { + RouteGraphNode rgn = connection.getHint(RouteGraphConnectionClass.KEY_RG_NODE); + RouteLine line = rgn.getRouteGraph().pickLine(x, y, pd); + if (line == null) + continue; + + Point2D intersection = intersectionPoint(x, y, line); + if (intersection == null) + continue; + + double dx = intersection.getX() - x; + double dy = intersection.getY() - y; + double dist = Math.sqrt(dx*dx + dy*dy); + if (dist < nearest) { + nearest = dist; + nearestTarget = new RouteGraphTarget(connection, rgn, line, new Point2D.Double(x, y), intersection); + } + } + + return nearestTarget; + } + + static Point2D intersectionPoint(double x, double y, RouteLine line) { + Collection points = line.getPoints(); + if (points.size() < 2) + return null; + RoutePoint s = line.getBegin(); + RoutePoint e = line.getEnd(); + return GeometryUtils.intersectionToLine(s.getX(), s.getY(), e.getX(), e.getY(), x, y); + } + } \ No newline at end of file