/******************************************************************************* * 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.g2d.diagram.participant.pointertool; import java.awt.AlphaComposite; import java.awt.Cursor; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Queue; import java.util.Set; import java.util.UUID; import org.simantics.g2d.canvas.Hints; import org.simantics.g2d.canvas.ICanvasContext; import org.simantics.g2d.canvas.IMouseCursorContext; import org.simantics.g2d.canvas.IMouseCursorHandle; 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.ConnectionEntity; import org.simantics.g2d.connection.handler.ConnectionHandler; import org.simantics.g2d.diagram.DiagramHints; import org.simantics.g2d.diagram.DiagramUtils; import org.simantics.g2d.diagram.IDiagram; import org.simantics.g2d.diagram.handler.Relationship; import org.simantics.g2d.diagram.handler.RelationshipHandler; import org.simantics.g2d.diagram.handler.RelationshipHandler.Relation; import org.simantics.g2d.diagram.handler.Topology; import org.simantics.g2d.diagram.handler.Topology.Connection; import org.simantics.g2d.diagram.handler.Topology.Terminal; import org.simantics.g2d.element.ElementClass; import org.simantics.g2d.element.ElementHints; import org.simantics.g2d.element.ElementUtils; import org.simantics.g2d.element.IElement; import org.simantics.g2d.element.handler.Move; import org.simantics.g2d.element.handler.TerminalTopology; import org.simantics.g2d.participant.RenderingQualityInteractor; import org.simantics.g2d.participant.TransformUtil; import org.simantics.scenegraph.ILookupService; import org.simantics.scenegraph.Node; import org.simantics.scenegraph.g2d.G2DParentNode; import org.simantics.scenegraph.g2d.events.Event; import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler; import org.simantics.scenegraph.g2d.events.MouseEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonEvent; 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.LinkNode; import org.simantics.scenegraph.g2d.nodes.SingleElementNode; import org.simantics.scenegraph.g2d.snap.ISnapAdvisor; import org.simantics.scenegraph.utils.NodeUtil; import org.simantics.scenegraph.utils.Quality; import org.simantics.utils.ObjectUtils; import org.simantics.utils.logging.TimeLogger; /** * This participant handles the diagram in translate elements mode. * * @author Toni Kalajainen * @author Tuukka Lehtonen */ public class TranslateMode extends AbstractMode { @Reference protected RenderingQualityInteractor quality; @Dependency protected TransformUtil util; protected Point2D startingPoint; protected Point2D currentPoint; protected IMouseCursorHandle cursor; protected Collection elementsToTranslate = Collections.emptyList(); protected Collection elementsToReallyTranslate = Collections.emptyList(); protected Collection translatedConnections = Collections.emptyList(); /** * A set of elements gathered during * {@link #getElementsToReallyTranslate(IDiagram, Collection)} that is to be * marked dirty after the translation operation has been completed. * *

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