--- /dev/null
+/*******************************************************************************\r
+ * Copyright (c) 2007, 2010 Association for Decentralized Information Management\r
+ * in Industry THTH ry.\r
+ * All rights reserved. This program and the accompanying materials\r
+ * are made available under the terms of the Eclipse Public License v1.0\r
+ * which accompanies this distribution, and is available at\r
+ * http://www.eclipse.org/legal/epl-v10.html\r
+ *\r
+ * Contributors:\r
+ * VTT Technical Research Centre of Finland - initial API and implementation\r
+ *******************************************************************************/\r
+package org.simantics.g2d.diagram.participant.pointertool;\r
+\r
+import java.awt.AlphaComposite;\r
+import java.awt.Cursor;\r
+import java.awt.geom.AffineTransform;\r
+import java.awt.geom.Point2D;\r
+import java.util.ArrayDeque;\r
+import java.util.ArrayList;\r
+import java.util.Collection;\r
+import java.util.Collections;\r
+import java.util.HashSet;\r
+import java.util.Queue;\r
+import java.util.Set;\r
+import java.util.UUID;\r
+\r
+import org.simantics.g2d.canvas.Hints;\r
+import org.simantics.g2d.canvas.ICanvasContext;\r
+import org.simantics.g2d.canvas.IMouseCursorContext;\r
+import org.simantics.g2d.canvas.IMouseCursorHandle;\r
+import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;\r
+import org.simantics.g2d.canvas.impl.DependencyReflection.Reference;\r
+import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;\r
+import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit;\r
+import org.simantics.g2d.connection.ConnectionEntity;\r
+import org.simantics.g2d.connection.handler.ConnectionHandler;\r
+import org.simantics.g2d.diagram.DiagramHints;\r
+import org.simantics.g2d.diagram.DiagramUtils;\r
+import org.simantics.g2d.diagram.IDiagram;\r
+import org.simantics.g2d.diagram.handler.Relationship;\r
+import org.simantics.g2d.diagram.handler.RelationshipHandler;\r
+import org.simantics.g2d.diagram.handler.RelationshipHandler.Relation;\r
+import org.simantics.g2d.diagram.handler.Topology;\r
+import org.simantics.g2d.diagram.handler.Topology.Connection;\r
+import org.simantics.g2d.diagram.handler.Topology.Terminal;\r
+import org.simantics.g2d.element.ElementClass;\r
+import org.simantics.g2d.element.ElementHints;\r
+import org.simantics.g2d.element.ElementUtils;\r
+import org.simantics.g2d.element.IElement;\r
+import org.simantics.g2d.element.handler.Move;\r
+import org.simantics.g2d.element.handler.TerminalTopology;\r
+import org.simantics.g2d.participant.RenderingQualityInteractor;\r
+import org.simantics.g2d.participant.TransformUtil;\r
+import org.simantics.scenegraph.ILookupService;\r
+import org.simantics.scenegraph.Node;\r
+import org.simantics.scenegraph.g2d.G2DParentNode;\r
+import org.simantics.scenegraph.g2d.events.Event;\r
+import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler;\r
+import org.simantics.scenegraph.g2d.events.MouseEvent;\r
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonEvent;\r
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent;\r
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;\r
+import org.simantics.scenegraph.g2d.events.command.CommandEvent;\r
+import org.simantics.scenegraph.g2d.events.command.Commands;\r
+import org.simantics.scenegraph.g2d.nodes.LinkNode;\r
+import org.simantics.scenegraph.g2d.nodes.SingleElementNode;\r
+import org.simantics.scenegraph.g2d.snap.ISnapAdvisor;\r
+import org.simantics.scenegraph.utils.NodeUtil;\r
+import org.simantics.scenegraph.utils.Quality;\r
+import org.simantics.utils.ObjectUtils;\r
+import org.simantics.utils.logging.TimeLogger;\r
+\r
+/**\r
+ * This participant handles the diagram in translate elements mode.\r
+ *\r
+ * @author Toni Kalajainen\r
+ * @author Tuukka Lehtonen\r
+ */\r
+public class TranslateMode extends AbstractMode {\r
+\r
+ @Reference\r
+ protected RenderingQualityInteractor quality;\r
+\r
+ @Dependency\r
+ protected TransformUtil util;\r
+\r
+ protected Point2D startingPoint;\r
+ protected Point2D currentPoint;\r
+ protected IMouseCursorHandle cursor;\r
+\r
+ protected Collection<IElement> elementsToTranslate = Collections.emptyList();\r
+ protected Collection<IElement> elementsToReallyTranslate = Collections.emptyList();\r
+ protected Collection<IElement> translatedConnections = Collections.emptyList();\r
+\r
+ /**\r
+ * A set of elements gathered during\r
+ * {@link #getElementsToReallyTranslate(IDiagram, Collection)} that is to be\r
+ * marked dirty after the translation operation has been completed.\r
+ * \r
+ * <p>\r
+ * This exists to cover cases where indirectly related (not topologically)\r
+ * elements are not properly updated after translation operations.\r
+ */\r
+ protected Collection<IElement> elementsToDirty = new HashSet<IElement>();\r
+\r
+ /**\r
+ * The node under which the mutated diagram is ghosted in the scene graph.\r
+ */\r
+ protected G2DParentNode parent;\r
+\r
+ /**\r
+ * This stays null until the translated diagram parts have been initialized\r
+ * into the scene graph. After that, only the translations of the nodes in\r
+ * the scene graph are modified.\r
+ */\r
+ protected SingleElementNode node = null;\r
+\r
+ protected double dx;\r
+ protected double dy;\r
+\r
+ public TranslateMode(Point2D startingPoint, Point2D currentPoint, int mouseId, Collection<IElement> elements) {\r
+ super(mouseId);\r
+ if (startingPoint == null)\r
+ throw new NullPointerException("null startingPoint");\r
+ if (currentPoint == null)\r
+ throw new NullPointerException("null currentPoint");\r
+ if (elements == null)\r
+ throw new NullPointerException("null elements");\r
+\r
+ this.startingPoint = startingPoint;\r
+ this.currentPoint = currentPoint;\r
+ this.elementsToTranslate = elements;\r
+ }\r
+\r
+ @Override\r
+ protected void onDiagramSet(IDiagram newDiagram, IDiagram oldDiagram) {\r
+ if (newDiagram != null) {\r
+// for (IElement e : elementsToTranslate) {\r
+// System.out.println("element: " + e);\r
+// }\r
+ processTranslatedSelection(newDiagram, elementsToTranslate);\r
+// for (IElement e : elementsToReallyTranslate) {\r
+// System.out.println("REAL element: " + e);\r
+// }\r
+\r
+ int i = 0;\r
+ ILookupService lookup = NodeUtil.getLookupService(node);\r
+ for (IElement e : elementsToReallyTranslate) {\r
+ Node n = e.getHint(ElementHints.KEY_SG_NODE);\r
+ if (n != null) {\r
+ LinkNode link = node.addNode("" + (i), LinkNode.class);\r
+ link.setZIndex(i);\r
+\r
+ String id = lookup.lookupId(n);\r
+ if (id == null) {\r
+ id = UUID.randomUUID().toString();\r
+ lookup.map(id, n);\r
+ link.setLookupIdOwner(true);\r
+ }\r
+ link.setDelegateId(id);\r
+\r
+ ++i;\r
+ }\r
+ }\r
+ }\r
+ }\r
+\r
+ protected void processTranslatedSelection(IDiagram diagram, Collection<IElement> elementsToTranslate) {\r
+ // Only translate elements that are not parents of another elements that\r
+ // is in the translated set also. Otherwise we would end up doing double\r
+ // translation for the parented elements.\r
+\r
+ // Don't move "connections only" selections.\r
+ int connectionCount = 0; \r
+ for (IElement e : elementsToTranslate) {\r
+ if (e.getElementClass().containsClass(ConnectionHandler.class)) {\r
+ ++connectionCount;\r
+ }\r
+ }\r
+ if (connectionCount == elementsToTranslate.size())\r
+ return;\r
+\r
+ elementsToReallyTranslate = new HashSet<IElement>(elementsToTranslate);\r
+ translatedConnections = new HashSet<IElement>();\r
+\r
+ // 1st: find out if some elements should not be translated\r
+ // because their parent elements are being translated also.\r
+ // \r
+ // Post-invariants:\r
+ // * elementsToReallyTranslate does not contain any elements whose parents are also translated\r
+ // * elementsToDirty contains all elements whose parents are also translated\r
+ RelationshipHandler erh = diagram.getDiagramClass().getAtMostOneItemOfClass(RelationshipHandler.class);\r
+ if (erh != null) {\r
+ Queue<Object> todo = new ArrayDeque<Object>(elementsToTranslate);\r
+ Set<Object> visited = new HashSet<Object>();\r
+ Collection<Relation> relations = new ArrayList<Relation>();\r
+ while (!todo.isEmpty()) {\r
+ Object e = todo.poll();\r
+ if (!visited.add(e))\r
+ continue;\r
+\r
+ // Check PARENT_OF relationships\r
+ relations.clear();\r
+ erh.getRelations(diagram, e, relations);\r
+ for (Relation r : relations) {\r
+ if (Relationship.PARENT_OF.equals(r.getRelationship())) {\r
+ elementsToReallyTranslate.remove(r.getObject());\r
+ if (r.getObject() instanceof IElement)\r
+ elementsToDirty.add((IElement) r.getObject());\r
+ if (!visited.contains(r.getObject()))\r
+ todo.add(r.getObject());\r
+ }\r
+ }\r
+\r
+ if (e instanceof IElement) {\r
+ IElement el = (IElement) e;\r
+\r
+ // Do not try to translate non-moveable elements.\r
+ if (!el.getElementClass().containsClass(Move.class)) {\r
+ elementsToReallyTranslate.remove(el);\r
+ continue;\r
+ }\r
+\r
+ // Check Parent handler\r
+ Collection<IElement> parents = ElementUtils.getParents(el);\r
+ boolean parentIsTranslated = false;\r
+ for (IElement parent : parents) {\r
+ if (elementsToTranslate.contains(parent)) {\r
+ parentIsTranslated = true;\r
+ break;\r
+ }\r
+ }\r
+ if (parentIsTranslated) {\r
+ elementsToReallyTranslate.remove(el);\r
+ elementsToDirty.add(el);\r
+ }\r
+ }\r
+ }\r
+ }\r
+\r
+ // 2nd: Include those connections in the translation for which all\r
+ // all terminal connected elements are also included in the selection.\r
+\r
+ Collection<Terminal> terminals = new ArrayList<Terminal>();\r
+ Collection<Connection> connections = new ArrayList<Connection>();\r
+ Collection<Connection> connections2 = new ArrayList<Connection>();\r
+\r
+ Topology topology = diagram.getDiagramClass().getSingleItem(Topology.class);\r
+ for (IElement el : new ArrayList<IElement>(elementsToReallyTranslate)) {\r
+ // Check if the selection is a connection.\r
+ // If it is, translate all of its branchpoints too,\r
+ // but only if all of its terminal connected elements\r
+ // are about to be translated too.\r
+ ElementClass ec = el.getElementClass();\r
+ TerminalTopology tt = ec.getAtMostOneItemOfClass(TerminalTopology.class);\r
+ if (tt != null) {\r
+ terminals.clear();\r
+ tt.getTerminals(el, terminals);\r
+ for (Terminal terminal : terminals) {\r
+ topology.getConnections(el, terminal, connections);\r
+ for (Connection conn : connections) {\r
+ ConnectionEntity ce = conn.edge.getHint(ElementHints.KEY_CONNECTION_ENTITY);\r
+ if (ce == null)\r
+ continue;\r
+ IElement connection = ce.getConnection();\r
+ if (connection == null)\r
+ continue;\r
+ ConnectionHandler ch = connection.getElementClass().getAtMostOneItemOfClass(ConnectionHandler.class);\r
+ if (ch == null)\r
+ continue;\r
+\r
+ connections2.clear();\r
+ ch.getTerminalConnections(connection, connections2);\r
+\r
+ boolean allConnectedNodesSelected = true;\r
+ for (Connection conn2 : connections2) {\r
+ if (!elementsToReallyTranslate.contains(conn2.node)) {\r
+ allConnectedNodesSelected = false;\r
+ break;\r
+ }\r
+ }\r
+\r
+ if (allConnectedNodesSelected) {\r
+ // Finally! It seems like all nodes connected\r
+ // with 'connection' are about to be translated.\r
+ // We should also include the whole connection\r
+ // in the translation.\r
+ elementsToReallyTranslate.add(connection);\r
+ translatedConnections.add(connection);\r
+ }\r
+ }\r
+ }\r
+ }\r
+ }\r
+\r
+ // Include all non-selected branch points in the translation that\r
+ // are either:\r
+ // a) among connections that have been either selected\r
+ // b) exist in a connection whose all terminal connected\r
+ // elements are selected.\r
+\r
+ for (IElement el : new ArrayList<IElement>(elementsToReallyTranslate)) {\r
+ // Check if the selection is a connection.\r
+ // If it is, translate all of its branchpoints too,\r
+ // but only if all of its terminal connected elements\r
+ // are about to be translated too.\r
+ ConnectionHandler ch = el.getElementClass().getAtMostOneItemOfClass(ConnectionHandler.class);\r
+ if (ch != null) {\r
+ boolean anyTerminalConnectionsSelected = false;\r
+ Collection<Connection> terminalConnections = new ArrayList<Connection>();\r
+ ch.getTerminalConnections(el, terminalConnections);\r
+ for (Connection conn : terminalConnections) {\r
+ if (elementsToTranslate.contains(conn.node)) {\r
+ anyTerminalConnectionsSelected = true;\r
+ break;\r
+ }\r
+ }\r
+\r
+ if (anyTerminalConnectionsSelected) {\r
+ translatedConnections.add(el);\r
+ Collection<IElement> branchPoints = new ArrayList<IElement>();\r
+ ch.getBranchPoints(el, branchPoints);\r
+ elementsToReallyTranslate.addAll(branchPoints);\r
+ }\r
+ }\r
+ }\r
+ }\r
+\r
+ @Override\r
+ public void addedToContext(ICanvasContext ctx) {\r
+ super.addedToContext(ctx);\r
+ if (quality != null)\r
+ quality.setStaticQuality(Quality.LOW);\r
+ IMouseCursorContext mcc = getContext().getMouseCursorContext();\r
+ cursor = mcc == null ? null : mcc.setCursor(mouseId, new Cursor(Cursor.MOVE_CURSOR));\r
+ }\r
+\r
+ @Override\r
+ public void removedFromContext(ICanvasContext ctx) {\r
+ if (cursor != null) {\r
+ cursor.remove();\r
+ cursor = null;\r
+ }\r
+ if (quality != null)\r
+ quality.setStaticQuality(null);\r
+ super.removedFromContext(ctx);\r
+ }\r
+\r
+ public AffineTransform getTransform() {\r
+ double dx = currentPoint.getX() - startingPoint.getX();\r
+ double dy = currentPoint.getY() - startingPoint.getY();\r
+ return AffineTransform.getTranslateInstance(dx, dy);\r
+ }\r
+\r
+ /**\r
+ * return translate in control coordinate\r
+ * @return\r
+ */\r
+ public Point2D getTranslateVector()\r
+ {\r
+ double dx = currentPoint.getX() - startingPoint.getX();\r
+ double dy = currentPoint.getY() - startingPoint.getY();\r
+ return new Point2D.Double(dx, dy);\r
+ }\r
+\r
+ @EventHandler(priority = 30)\r
+ public boolean handleEvent(Event e) {\r
+ // Reject all mouse events not for this mouse.\r
+ if (e instanceof MouseEvent) {\r
+ MouseEvent me = (MouseEvent) e;\r
+ if (me.mouseId != mouseId)\r
+ return false;\r
+ }\r
+\r
+ if (e instanceof CommandEvent) {\r
+ CommandEvent event = (CommandEvent) e;\r
+ if (event.command.equals( Commands.CANCEL)) {\r
+ setDirty();\r
+ remove();\r
+ return true;\r
+ } else if (event.command.equals( Commands.ROTATE_ELEMENT_CCW )\r
+ || event.command.equals( Commands.DELETE )\r
+ || event.command.equals( Commands.ROTATE_ELEMENT_CW )\r
+ || event.command.equals( Commands.FLIP_ELEMENT_HORIZONTAL )\r
+ || event.command.equals( Commands.FLIP_ELEMENT_VERTICAL) ) {\r
+ // Just eat these commands to disable\r
+ // rotation and scaling during translation.\r
+ return true;\r
+ }\r
+ } else if (e instanceof MouseMovedEvent) {\r
+ return move((MouseMovedEvent) e);\r
+ } else if (e instanceof MouseButtonReleasedEvent) {\r
+ if (((MouseButtonEvent)e).button == MouseEvent.LEFT_BUTTON) {\r
+ TimeLogger.resetTimeAndLog(getClass(), "handleEvent");\r
+ return commit();\r
+ }\r
+ }\r
+ return false;\r
+ }\r
+\r
+ protected boolean move(MouseMovedEvent event) {\r
+ Point2D canvasPos = util.controlToCanvas(event.controlPosition, null);\r
+ if (ObjectUtils.objectEquals(currentPoint, canvasPos)) return true;\r
+\r
+ ISnapAdvisor snapAdvisor = getHint(DiagramHints.SNAP_ADVISOR);\r
+ if (snapAdvisor != null) {\r
+ IElement someElement = null;\r
+\r
+ for (IElement elem : elementsToReallyTranslate) {\r
+ someElement = elem;\r
+ break;\r
+ }\r
+\r
+ if (someElement != null) {\r
+ AffineTransform at = node.getTransform();\r
+ Move m = someElement.getElementClass().getSingleItem(Move.class);\r
+ Point2D oldPos = m.getPosition(someElement);\r
+ oldPos.setLocation(oldPos.getX() + at.getTranslateX(), oldPos.getY() + at.getTranslateY());\r
+\r
+ snapAdvisor.snap(canvasPos,\r
+ new Point2D[] {\r
+ new Point2D.Double(\r
+ -oldPos.getX() + currentPoint.getX(),\r
+ -oldPos.getY() + currentPoint.getY()\r
+ )\r
+ });\r
+ }\r
+ }\r
+\r
+ dx = canvasPos.getX()-startingPoint.getX();\r
+ dy = canvasPos.getY()-startingPoint.getY();\r
+ if ((event.stateMask & MouseEvent.CTRL_MASK) != 0) {\r
+ // Forced horizontal/vertical -only movement\r
+ if (Math.abs(dx) >= Math.abs(dy)) {\r
+ dy = 0;\r
+ } else {\r
+ dx = 0;\r
+ }\r
+ }\r
+\r
+ AffineTransform nat = AffineTransform.getTranslateInstance(dx, dy);\r
+ node.setTransform(nat);\r
+\r
+ currentPoint = canvasPos;\r
+\r
+ setDirty();\r
+ return true;\r
+ }\r
+\r
+ protected boolean commit() {\r
+ TimeLogger.resetTimeAndLog(getClass(), "commit");\r
+ for (IElement el : elementsToReallyTranslate) {\r
+ Move move = el.getElementClass().getAtMostOneItemOfClass(Move.class);\r
+ if (move != null) {\r
+ Point2D oldPos = move.getPosition(el);\r
+ move.moveTo(el, oldPos.getX() + dx, oldPos.getY() + dy);\r
+ }\r
+ }\r
+\r
+ // Persist all translation modifications.\r
+ DiagramUtils.mutateDiagram(diagram, m -> {\r
+ for (IElement e : elementsToReallyTranslate)\r
+ m.modifyTransform(e);\r
+ });\r
+\r
+ for (IElement dirty : elementsToDirty)\r
+ dirty.setHint(Hints.KEY_DIRTY, Hints.VALUE_SG_DIRTY);\r
+\r
+ setDirty();\r
+ remove();\r
+ return false;\r
+ }\r
+\r
+ @SGInit\r
+ public void initSG(G2DParentNode parent) {\r
+ this.parent = parent;\r
+ node = parent.addNode("translate ghost", SingleElementNode.class);\r
+ node.setZIndex(1000);\r
+ node.setVisible(Boolean.TRUE);\r
+ node.setComposite(AlphaComposite.SrcOver.derive(0.4f));\r
+ }\r
+\r
+ @SGCleanup\r
+ public void cleanupSG() {\r
+ if (node != null) {\r
+ node.remove();\r
+ node = null;\r
+ }\r
+ parent = null;\r
+ }\r
+\r
+}\r