/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.diagram.handler; import java.awt.Color; import java.awt.event.KeyEvent; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.action.IStatusLineManager; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IPartListener; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchPartSite; import org.eclipse.ui.PlatformUI; import org.simantics.Simantics; import org.simantics.databoard.Bindings; import org.simantics.db.ReadGraph; import org.simantics.db.Resource; import org.simantics.db.WriteGraph; import org.simantics.db.common.request.WriteRequest; import org.simantics.db.common.utils.OrderedSetUtils; import org.simantics.db.exception.DatabaseException; import org.simantics.db.layer0.util.ClipboardUtils; import org.simantics.db.layer0.util.SimanticsClipboard; import org.simantics.db.layer0.util.SimanticsClipboard.Representation; import org.simantics.db.layer0.util.SimanticsKeys; import org.simantics.db.layer0.variable.Variable; import org.simantics.db.layer0.variable.Variables; import org.simantics.db.request.Read; import org.simantics.diagram.Logger; import org.simantics.diagram.content.Change; import org.simantics.diagram.content.ConnectionUtil; import org.simantics.diagram.content.DiagramContentChanges; import org.simantics.diagram.content.DiagramContentTracker; import org.simantics.diagram.internal.Activator; import org.simantics.diagram.stubs.DiagramResource; import org.simantics.diagram.stubs.G2DResource; import org.simantics.diagram.synchronization.graph.DiagramGraphUtil; import org.simantics.diagram.synchronization.runtime.DiagramSelectionUpdater; import org.simantics.diagram.ui.DiagramModelHints; import org.simantics.g2d.canvas.ICanvasContext; import org.simantics.g2d.canvas.impl.AbstractCanvasParticipant; 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.handler.ConnectionHandler; import org.simantics.g2d.diagram.IDiagram; 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.diagram.participant.AbstractDiagramParticipant; import org.simantics.g2d.diagram.participant.Selection; 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.BendsHandler; import org.simantics.g2d.element.handler.EdgeVisuals.EdgeEnd; import org.simantics.g2d.element.handler.Move; import org.simantics.g2d.element.handler.TerminalTopology; import org.simantics.g2d.element.handler.Transform; import org.simantics.g2d.elementclass.FlagHandler; import org.simantics.g2d.participant.GridPainter; import org.simantics.g2d.participant.MouseUtil; import org.simantics.g2d.participant.MouseUtil.MouseInfo; import org.simantics.layer0.Layer0; import org.simantics.modeling.ModelingResources; import org.simantics.operation.Layer0X; import org.simantics.project.IProject; import org.simantics.scenegraph.INode; import org.simantics.scenegraph.ParentNode; import org.simantics.scenegraph.g2d.G2DParentNode; import org.simantics.scenegraph.g2d.IG2DNode; import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler; import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent; import org.simantics.scenegraph.g2d.events.KeyEvent.KeyReleasedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseEnterEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseExitEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent; import org.simantics.scenegraph.g2d.events.command.Command; 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.LocalDelegateNode; import org.simantics.scenegraph.g2d.nodes.SingleElementNode; import org.simantics.scenegraph.utils.NodeMapper; import org.simantics.utils.datastructures.collections.CollectionUtils; import org.simantics.utils.datastructures.hints.HintListenerAdapter; import org.simantics.utils.datastructures.hints.IHintContext.Key; import org.simantics.utils.datastructures.hints.IHintContext.KeyOf; import org.simantics.utils.datastructures.hints.IHintListener; import org.simantics.utils.datastructures.hints.IHintObservable; import org.simantics.utils.logging.TimeLogger; import org.simantics.utils.threads.SWTThread; import org.simantics.utils.threads.ThreadUtils; import org.simantics.utils.ui.ErrorLogger; import org.simantics.utils.ui.SWTUtils; /** * CopyPasteHandler is a canvas handler for Commands.CUT, Commands.COPY and * Commands.PASTE commands for an IDiagram. * *

* The handler attempts to copy/paste the current selection for pointer 0, * meaning {@link Selection#SELECTION0}. *

* *

* The handler logic follows the specifications at UC:Copy Item * and UC:Cut * Item. *

* * @see Selection current diagram selection source * * @author Tuukka Lehtonen * * FIXME: translucent ghosting makes rendering REALLY sluggish, add a * timer that makes the ghost opaque when the user is interacting and * translucent only when still for a while. */ public class CopyPasteHandler extends AbstractDiagramParticipant { public static final Key KEY_CUT_SELECTION_FRAME_COLOR = new KeyOf(Color.class, "CUT_SELECTION_FRAME_COLOR"); public static final Key KEY_CUT_SELECTION_CONTENT_COLOR = new KeyOf(Color.class, "CUT_SELECTION_CONTENT_COLOR"); public static final Key KEY_COPIED_SELECTION_FRAME_COLOR = new KeyOf(Color.class, "COPY_SELECTION_FRAME_COLOR"); public static final Key KEY_COPIED_SELECTION_CONTENT_COLOR = new KeyOf(Color.class, "COPY_SELECTION_CONTENT_COLOR"); /** * A key for storing the current selection within the currently active * project for copy/paste implementation. */ private static final Key KEY_DIAGRAM_SELECTION = DiagramSelectionRepresentation.KEY_DIAGRAM_SELECTION; private static final boolean DEBUG = false; private static final boolean DEBUG_SELECTION_UPDATE = false; public static final int COPY_GHOSTING_PAINT_PRIORITY = 600; protected static final int HIGHLIGHT_PAINT_PRIORITY = 500; @Dependency protected Selection sel; @Dependency protected MouseUtil mouseUtil; protected final IStatusLineManager statusLine; protected final CopyPasteStrategy strategy; protected IWorkbenchPartSite site; protected IWorkbenchPartSite listenedSite; /** * Workbench part listener for {@link #listenedSite} to keep proper track of * whether this part is focused or not. */ IPartListener partListener = new IPartListener() { @Override public void partOpened(IWorkbenchPart part) { } @Override public void partDeactivated(IWorkbenchPart part) { if (part == site.getPart()) hasFocus = false; } @Override public void partClosed(IWorkbenchPart part) { // Make sure this listener is removed properly in any case. if (listenedSite != null) { listenedSite.getPage().removePartListener(partListener); listenedSite = null; } } @Override public void partBroughtToTop(IWorkbenchPart part) { } @Override public void partActivated(IWorkbenchPart part) { if (part == site.getPart()) hasFocus = true; } }; /** * Indicates whether CopyPasteHandler thinks that {@link #site} has focus. */ protected boolean hasFocus = false; protected AbstractCanvasParticipant highlightMode = null; private IProject observedProject = null; /** * A counter for how many times pasting has been performed without mouse and * ghosting or how many times paste has been performed without moving the * mouse on the diagram. This is used to offset the paste position * accordingly so that copied elements don't wind up directly on top of each * other. */ private int pasteWithoutMovingGhostCounter = 0; /** * An offset used when pasting without mouse/ghosting. It forces keyboard * pastes to stack up on top of the latest paste performed with * mouse/ghosting. */ private final Point2D pasteOffset = new Point2D.Double(0, 0); /** * Stores the last MouseInfo for mouse 0 from the time of the previous * received mouse event. Used for deciding the paste position. * * @see #getPastePos(DiagramSelection) */ private MouseInfo mouseInfo; /** * Scale to use for pasted diagram monitors from variables. */ private double monitorScale = 0.2; /** * For updating the diagram selection after graph changes. */ private DiagramSelectionUpdater selectionUpdater = null; public CopyPasteHandler() { this(new DefaultCopyPasteStrategy()); } public CopyPasteHandler(CopyPasteStrategy strategy) { this(strategy, null); } public CopyPasteHandler(IStatusLineManager statusLine) { this(new DefaultCopyPasteStrategy(), statusLine); } public CopyPasteHandler(CopyPasteStrategy strategy, IStatusLineManager statusLine) { this.strategy = strategy != null ? strategy : new DefaultCopyPasteStrategy(); this.statusLine = statusLine; } public CopyPasteHandler(CopyPasteStrategy strategy, IStatusLineManager statusLine, double monitorScale) { this.strategy = strategy != null ? strategy : new DefaultCopyPasteStrategy(); this.statusLine = statusLine; setMonitorScale(monitorScale); } public CopyPasteHandler setMonitorScale(double scale) { this.monitorScale = scale; return this; } public CopyPasteHandler setWorkbenchSite(IWorkbenchPartSite site) { this.site = site; return this; } protected boolean isPasteAllowed() { return listenedSite == null || hasFocus; } @Override public void addedToContext(ICanvasContext ctx) { super.addedToContext(ctx); addProjectListener(peekProject()); listenedSite = site; if (listenedSite != null) { listenedSite.getPage().addPartListener(partListener); } } @Override public void removedFromContext(ICanvasContext ctx) { // Remove project selection if its ours to prevent leaking memory. DiagramSelection ds = getProjectSelection(); if (ds.getSourceCanvas() == ctx) { removeProjectSelection(); } if (listenedSite != null) { listenedSite.getPage().removePartListener(partListener); listenedSite = null; } removeProjectListener(); super.removedFromContext(ctx); } @Override protected void onDiagramSet(IDiagram newDiagram, IDiagram oldDiagram) { if (oldDiagram != null) { if (selectionUpdater != null) { selectionUpdater.untrack(); selectionUpdater = null; } } if (newDiagram != null) { selectionUpdater = new DiagramSelectionUpdater(getContext(), newDiagram).track(); } } IHintListener projectDiagramSelectionListener = new HintListenerAdapter() { @Override public void hintChanged(IHintObservable sender, Key key, Object oldValue, final Object newValue) { //System.out.println(this + ": " + sender + ": " + newValue); ICanvasContext ctx = getContext(); if (ctx != null && hasHighlight()) { //System.out.println(this + " HAS HIGHLIGHT"); if (newValue == null || ((DiagramSelection) newValue).getSourceCanvas() != ctx) { //System.out.println(this + " REMOVING HIGHLIGHT"); ctx.getThreadAccess().asyncExec(new Runnable() { @Override public void run() { removeHighlight(); } }); } } } }; private void addProjectListener(IProject observable) { if (observable != null) { observable.addKeyHintListener(KEY_DIAGRAM_SELECTION, projectDiagramSelectionListener); observedProject = observable; } } private void removeProjectListener() { if (observedProject != null) { observedProject.removeKeyHintListener(KEY_DIAGRAM_SELECTION, projectDiagramSelectionListener); observedProject = null; } } IProject getProject() { return Simantics.getProject(); } IProject peekProject() { return Simantics.peekProject(); } public DiagramSelection getClipboardDiagramSelection() { for (Set content : Simantics.getClipboard().getContents()) { try { DiagramSelection sel = ClipboardUtils.accept(content, DiagramSelectionRepresentation.KEY_DIAGRAM_SELECTION); if (sel != null) return sel; } catch (DatabaseException e) { Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Failed to retrieve clipboard content.", e)); } } return DiagramSelection.EMPTY; } protected DiagramSelection getProjectSelection() { IProject p = peekProject(); if (p == null) return DiagramSelection.EMPTY; DiagramSelection ds = p.getHint(KEY_DIAGRAM_SELECTION); return ds != null ? ds : DiagramSelection.EMPTY; } protected void setDiagramSelection(DiagramSelection selection) { setProjectSelection(selection); strategy.copyToClipboard(selection); } protected void setProjectSelection(DiagramSelection selection) { assert selection != null; IProject p = getProject(); if (p == null) throw new IllegalStateException("no active project for selection"); clearSG(); pasteWithoutMovingGhostCounter = 0; pasteOffset.setLocation(0, 0); p.setHint(KEY_DIAGRAM_SELECTION, selection); } protected void removeProjectSelection() { setProjectSelection(DiagramSelection.EMPTY); removeHighlight(); clearSG(); setDirty(); } /** * This DELETE command handler is required to remove any diagram selections * when modules are deleted. This will prevent extra highlight painting from * being left over in case a module marked for copying is deleted. */ @EventHandler(priority = 100) public boolean handleDelete(CommandEvent e) { if (e.command.equals( Commands.DELETE) ) { if (highlightMode != null) { message(null); removeProjectSelection(); return false; } } return false; } @EventHandler(priority = 0) public boolean handleKey(org.simantics.scenegraph.g2d.events.KeyEvent e) { if (e.keyCode == KeyEvent.VK_CONTROL) { DiagramSelection ds = getProjectSelection(); if (!ds.isEmpty()) { if (e instanceof KeyPressedEvent) { if (ds.isCut()) message("Move selection"); else message("Paste selection"); updateSG(ds); } else if (e instanceof KeyReleasedEvent) { selectedMessage(ds); hideSG(ds); } setDirty(); } } return false; } @EventHandler(priority = 0) public boolean handleCommand(CommandEvent e) { if (e.command.equals( Commands.CANCEL) ) { DiagramSelection s = getProjectSelection(); if (highlightMode != null || !s.isEmpty()) { message(null); removeProjectSelection(); return true; } return false; } if (e.command.equals( Commands.CUT ) || e.command.equals( Commands.COPY )) { boolean ret = initiateCopy( e.command.equals( Commands.CUT ) ); if (!ret) removeProjectSelection(); return ret; } // Must have focus in order to paste! If mouse has left the editor, no // pastes should be performed. if (isPasteAllowed() && e.command.equals( Commands.PASTE )) { DiagramSelection ds = getClipboardDiagramSelection(); if (ds.isEmpty()) { return tryPasteMonitors(); } return paste(e.command, ds); } return false; } protected boolean initiateCopy(boolean cut) { //System.out.println("INITIATING COPY"); int selectionId = 0; Set ss = sel.getSelection(selectionId); Point2D copyPos = getCopyStartPos(ss); if (ss.isEmpty() || copyPos == null) { message("Nothing to " + (cut ? "cut" : "copy")); return false; } // Validate selection, don't initiate copy if selection is invalid. ElementAssortment ea = new ElementAssortment(ss); String error = fixAssortment(ea, cut); if (error != null) { message(error); return false; } pruneAssortment(ea, cut); if (ea.isEmpty()) { message("Nothing to " + (cut ? "cut" : "copy")); return false; } // Allow the possible CopyStrategy to filter the copied element // assortment also. if (strategy instanceof CopyStrategy) { if (DEBUG) System.out.println("Commencing CopyStrategy filtering with " + ea); IStatus status = ((CopyStrategy) strategy).copy(new CopyOperation(getContext(), ea, cut)); if (!status.isOK()) { switch (status.getSeverity()) { case IStatus.CANCEL: case IStatus.WARNING: message(status.getMessage()); break; case IStatus.ERROR: error(status.getMessage()); break; } return false; } } if (DEBUG) System.out.println("Start copy with " + ea); // Treat OTHER type elements as disconnected floating graphical elements // that are always copied. // Anything with connection parts cannot be copied if (!cut && ea.containsAny(CopyPasteUtil.CONNECTION_PARTS)) { error("Cannot copy connection segments nor branch points."); return false; } // if (ea.contains(CopyPasteUtil.MONITORS)) { // // TODO: allow copying of monitors, means fixing the component reference relations // error("Monitor " + (cut ? "cut" : "copy") + " not supported yet."); // return false; // } // Pre-validate flag selection cases if (ea.contains(CopyPasteUtil.FLAGS)) { if (cut) { // Allow cutting of single flags or cutting of any amount of // flags within a single diagram. } else { // // Deny flag copy if other kinds of elements are selected. // if (ea.containsAny(NOT_FLAGS)) { // return false; // } // Only copy flags without correspondence for now. if (CopyPasteUtil.isFlagsOnlySelection(ea)) { if (!CopyPasteUtil.checkFlagsCorrespondences(ea.flags, false)) { error("Cannot copy flag that already has a correspondence."); return false; } } } } // Selection is valid, go ahead and initiate a copy operation. Resource sourceDiagram = diagram.getHint(DiagramModelHints.KEY_DIAGRAM_RESOURCE); DiagramSelection ds = new DiagramSelection(getContext(), sourceDiagram, ea.getAll(), cut, copyPos); setDiagramSelection(ds); removeHighlight(); highlightMode = new HighlightMode(ds, selectionId, HIGHLIGHT_PAINT_PRIORITY); getContext().add(highlightMode); selectedMessage(ds); // System.out.println("INITIATED COPY: " + ds); return true; } public boolean paste(Command command, DiagramSelection ds) { if (ds.isEmpty()) { message(null); return false; } TimeLogger.resetTimeAndLog(getClass(), "paste"); ElementObjectAssortment ea = ds.getAssortment(); if (DEBUG) System.out.println("Initiate paste with " + ea); try { if (CopyPasteUtil.isFlagsOnlySelection(ea)) { // Do not copy if any of the flags already have a correspondence. if (!CopyPasteUtil.onlyFlagsWithoutCorrespondence(Simantics.getSession(), ea)) return true; if (ds.isCut()) { normalPaste(command, ds, ea, true); removeHighlight(); setDiagramSelection(DiagramSelection.EMPTY); resetSourceSelection(ds); } else { normalPaste(command, ds, ea, false); // There is no point in leaving the old copy selection hanging // around after copying a flag since it is a one shot operation. removeHighlight(); setDiagramSelection(DiagramSelection.EMPTY); } } else { if (ds.isCut()) { normalPaste(command, ds, ea, true); removeHighlight(); setDiagramSelection(DiagramSelection.EMPTY); resetSourceSelection(ds); } else { normalPaste(command, ds, ea, false); // // This is necessary to keep the ghost diagram properly up-to-date // // after paste operations. // setProjectSelection(ds.remutate()); } } message(null); } catch (PasteException e) { error( e.getLocalizedMessage() ); ErrorLogger.defaultLog( new Status(IStatus.INFO, Activator.PLUGIN_ID, "Problem in diagram paste operation, see exception for details.", e) ); } catch (DatabaseException e) { error( e.getLocalizedMessage() ); ErrorLogger.defaultLog( new Status(IStatus.INFO, Activator.PLUGIN_ID, "Problem in diagram paste operation, see exception for details.", e) ); } // Clear ghosting clearSG(); setDirty(); return true; } /** * In cut/paste cases, the source selection should reset (removed) after the * paste operation has been performed. This method will reset the source * selection of the specified DiagramSelection from the source canvas * context. This will work regardless of which diagram/editor the selection * originates from. * * @param ds the source selection to reset */ void resetSourceSelection(DiagramSelection ds) { ICanvasContext cc = ds.getSourceCanvas(); boolean sameDiagram = diagram == ds.getSourceDiagram(); if (!sameDiagram && cc != null && !cc.isDisposed()) { for (Selection sourceSelection : cc.getItemsByClass(Selection.class)) { Collection empty = Collections.emptySet(); sourceSelection.setSelection(0, empty); } } } private void normalPaste(Command command, DiagramSelection ds, ElementObjectAssortment ea, boolean cut) throws PasteException { final Point2D copyPos = ds.getCopyPos(); final Point2D pastePos = getPastePos(ds); double dx = pastePos.getX() - copyPos.getX(); double dy = pastePos.getY() - copyPos.getY(); final Point2D pasteOffset = new Point2D.Double(dx, dy); try { // Get diagram contents before the paste operation Resource diagramResource = diagram.getHint(DiagramModelHints.KEY_DIAGRAM_RESOURCE); final DiagramContentTracker tracker = diagramResource == null ? null : DiagramContentTracker.start(getContext(), Simantics.getSession(), diagramResource); strategy.paste(new PasteOperation(command, getContext(), ds.getSourceDiagram(), diagramResource, diagram, ea, cut, pasteOffset)); if (tracker != null) { // Get difference of diagram contents to find out what was added. DiagramContentChanges changes = tracker.update(); selectionUpdater.setNewSelection(0, changes.pick(changes.elements, Change.ADDED)); if (DEBUG_SELECTION_UPDATE) System.out.println("stored diagram changes @" + System.currentTimeMillis() + ": " + selectionUpdater.getNewSelection()); } } catch (DatabaseException e) { ErrorLogger.defaultLogError(e); } } protected String fixAssortment(ElementAssortment ea, boolean cut) { Topology diagramTopology = diagram.getDiagramClass().getAtMostOneItemOfClass(Topology.class); List conns = new ArrayList(); // Include flags whether they are selected or not for (IElement edge : ea.edges) { Connection bc = diagramTopology.getConnection(edge, EdgeEnd.Begin); if (bc != null && bc.node != null) { if (bc.node.getElementClass().getAtMostOneItemOfClass(FlagHandler.class) != null) ea.add(ElementType.Flag, bc.node); } Connection ec = diagramTopology.getConnection(edge, EdgeEnd.End); if (ec != null && ec.node != null) { if (ec.node.getElementClass().getAtMostOneItemOfClass(FlagHandler.class) != null) ea.add(ElementType.Flag, ec.node); } } // Include connections for selected flags if we're not potentially // making flag continuations. if (!CopyPasteUtil.isFlagsOnlySelection(ea)) { for (IElement flag : ea.flags) { conns.clear(); diagramTopology.getConnections(flag, ElementUtils.getSingleTerminal(flag), conns); for (Connection conn : conns) { IElement edge = conn.edge; ConnectionEntity ce = edge.getHint(ElementHints.KEY_CONNECTION_ENTITY); ea.add(ElementType.Connection, ce.getConnection()); } } } // For each selected connection, make sure that all connected elements // are in the selection, otherwise don't copy the connection. List connectionsToRemove = new ArrayList(ea.connections.size()); for (IElement connection : ea.connections) { ConnectionHandler ch = connection.getElementClass().getSingleItem(ConnectionHandler.class); Collection connectors = ch.getTerminalConnections(connection, null); boolean allConnectorsSelected = true; for (Connection c : connectors) { if (!(ea.nodes.contains(c.node) || ea.flags.contains(c.node) || ea.references.contains(c.node))) { allConnectorsSelected = false; break; } } if (!allConnectorsSelected) connectionsToRemove.add(connection); } ea.removeAll(ElementType.Connection, connectionsToRemove); // Remove external flags whose connection(s) are not included List flagsToRemove = new ArrayList(ea.flags.size()); for (IElement flag : ea.flags) { if (CopyPasteUtil.flagIsExternal(flag)) { conns.clear(); diagramTopology.getConnections(flag, ElementUtils.getSingleTerminal(flag), conns); for (Connection conn : conns) { IElement edge = conn.edge; ConnectionEntity ce = edge.getHint(ElementHints.KEY_CONNECTION_ENTITY); IElement connection = ce.getConnection(); if (!ea.connections.contains(connection)) { flagsToRemove.add(flag); } } } } ea.removeAll(ElementType.Flag, flagsToRemove); if (cut) { // Issue #1874: Prevent cut/paste for connected components // https://www.simulationsite.net/redmine/issues/1874 // Fail if any of the included nodes has connections to it that are not // included in the operation. Collection connections = new ArrayList(); for (IElement node : CollectionUtils.join(ea.nodes, ea.flags)) { connections.clear(); for (Connection connection : getAllConnections(node, connections)) { ConnectionEntity ce = connection.edge.getHint(ElementHints.KEY_CONNECTION_ENTITY); IElement conn = ce.getConnection(); if (ea.connections.contains(conn)) continue; return "Cannot cut a node without all its connections."; } } } // Remove all reference elements from the assortment whose parent elements are not in it. if (!cut) { Collection referenceElementsToRemove = new ArrayList(); for (IElement ref : ea.references) { IElement parent = ref.getHint(ElementHints.KEY_PARENT_ELEMENT); if (parent != null) { if (!ea.all.contains(parent)) { // Cannot copy reference element whose parent is not copied also. referenceElementsToRemove.add(ref); } } else { // OK, reference element has no parent. Free to copy/cut in any way. } } if (!referenceElementsToRemove.isEmpty()) { ea.removeAll(ElementType.Reference, referenceElementsToRemove); if (ea.isEmpty()) { return "Cannot copy reference elements whose parent is not copied."; } } } return null; } private Collection getTerminals(IElement node) { ArrayList result = new ArrayList(); for (TerminalTopology tt : node.getElementClass().getItemsByClass(TerminalTopology.class)) tt.getTerminals(node, result); return result; } private Collection getAllConnections(IElement node, Collection result) { IDiagram diagram = node.getDiagram(); Topology topology = diagram.getDiagramClass().getAtMostOneItemOfClass(Topology.class); if (topology == null) return result; for (Terminal t : getTerminals(node)) topology.getConnections(node, t, result); return result; } /** * Classifies the specified diagram selection elements into categories * appointed by the ElementType enumeration and prunes any * edges from the selection returned assortment whose both ends are not * connected to nodes within the selection. * * @param ea * @return */ protected void pruneAssortment(ElementAssortment ea, boolean cut) { // Edges and branch points are never copied as such. // They are always included as parts of copied connections. // Edges can never be transformed or modified in any way as such, // therefore it is safe to do this. ea.clear(ElementType.Edge); if (!cut) ea.clear(ElementType.BranchPoint); } private Point2D getPastePos(DiagramSelection ds) { MouseInfo mi = mouseUtil.getMouseInfo(0); if (mi == null) mi = mouseInfo; if (mi != null) { double xoff = mi.canvasPosition.getX() - ds.getCopyPos().getX(); double yoff = mi.canvasPosition.getY() - ds.getCopyPos().getY(); if (xoff == pasteOffset.getX() && yoff == pasteOffset.getY()) { // The mouse has not moved since last paste so let's offset the // paste down and right. double counterOffset = getOffsetGridSize() * (++pasteWithoutMovingGhostCounter); return new Point2D.Double( mi.canvasPosition.getX() + counterOffset, mi.canvasPosition.getY() + counterOffset); } pasteWithoutMovingGhostCounter = 0; pasteOffset.setLocation(xoff, yoff); return mi.canvasPosition; } else { //return ds.getCopyPos(); Point2D p = ds.getCopyPos(); double counterOffset = getOffsetGridSize() * (++pasteWithoutMovingGhostCounter); return new Point2D.Double( p.getX() + pasteOffset.getX() + counterOffset, p.getY() + pasteOffset.getY() + counterOffset); } } private double getOffsetGridSize() { Double grid = getHint(GridPainter.KEY_GRID_SIZE); return (grid == null || grid == 0) ? 1.0 : grid; } /** * @param e * @return */ protected static boolean isConnectionOrEdge(IElement e) { ElementClass ec = e.getElementClass(); return ec.containsClass(ConnectionHandler.class)|| ec.containsClass(BendsHandler.class); } /** * @param e * @return */ protected static boolean isMoveable(IElement e) { ElementClass ec = e.getElementClass(); return ec.containsClass(Move.class) && ec.containsClass(Transform.class); } /** * @param ss * @return null if a point of reference cannot be determined * for the specified selection. */ protected Point2D getCopyStartPos(Set ss) { // MouseInfo mi = mouseUtil.getMouseInfo(0); // if (mi != null) { // return (Point2D) mi.canvasPosition.clone(); // } // Find bounding rectangle top left corner double mx = Double.MAX_VALUE; double my = Double.MAX_VALUE; for (IElement e : ss) { if (isConnectionOrEdge(e) || !isMoveable(e)) continue; //Point2D pos = ElementUtils.getPos(e); Point2D pos = ElementUtils.getAbsolutePos(e); if (pos.getX() < mx) mx = pos.getX(); if (pos.getY() < my) my = pos.getY(); } // Find element nearest to the top left corner Point2D nearest = null; double dist = Double.MAX_VALUE; for (IElement e : ss) { if (isConnectionOrEdge(e) || !isMoveable(e)) continue; Point2D pos = ElementUtils.getAbsolutePos(e); double dx = pos.getX() - mx; double dy = pos.getY() - my; double d = dx*dx + dy*dy; if (d < dist) { dist = d; nearest = pos; } } return nearest; } private void moveGhostElements(DiagramSelection ds, Point2D pastePos) { Point2D copyPos = ds.getCopyPos(); double dx = (pastePos.getX() - copyPos.getX()); double dy = (pastePos.getY() - copyPos.getY()); // Snap delta Point2D snap = CopyPasteUtil.snap(getContext(), new Point2D.Double(dx, dy)); ghostNode.setTransform(AffineTransform.getTranslateInstance(snap.getX(), snap.getY())); //System.out.println("ghost node: " + ghostNode); } protected SingleElementNode ghostNode = null; protected NodeMapper ghostNodeMapper = new NodeMapper(); @SGInit public void initSG(G2DParentNode parent) { ghostNode = parent.addNode("cut/copy ghost", SingleElementNode.class); ghostNode.setZIndex(COPY_GHOSTING_PAINT_PRIORITY); //ghostNode.setComposite(AlphaComposite.SrcOver.derive(0.40f)); ghostNode.setVisible(Boolean.FALSE); } @SGCleanup public void cleanupSG() { ghostNode.remove(); } void clearSG() { ghostNode.removeNodes(); ghostNode.setVisible(Boolean.FALSE); ghostNodeMapper.clear(); } /** * @param selection * @return true if the ghost nodes were hidden and a refresh is * needed */ boolean hideSG(DiagramSelection selection) { if (ghostNode.isVisible()) { // Make sure there's no leftover graphics. ghostNode.removeNodes(); ghostNode.setVisible(Boolean.FALSE); return true; } return false; } protected void scheduleActivateOwnerPart() { if (site == null) return; SWTUtils.asyncExec(PlatformUI.getWorkbench().getDisplay(), new Runnable() { @Override public void run() { hasFocus = true; site.getPage().activate(site.getPart()); } }); } @EventHandler(priority = 0) public boolean handleMouse(MouseExitEvent e) { DiagramSelection ds = getProjectSelection(); if (!ds.isEmpty()) { if (hideSG(ds)) setDirty(); } // The part might no longer have focus. // [Tuukka] commented out to fix Apros #3678 //hasFocus = false; return false; } @EventHandler(priority = 0) public boolean handleMouse(MouseEnterEvent e) { DiagramSelection ds = getProjectSelection(); if (!ds.isEmpty()) { if (site != null) { if (inPasteMode(e)) { scheduleActivateOwnerPart(); } } } return false; } @EventHandler(priority = 0) public boolean handleMouse(MouseMovedEvent e) { DiagramSelection ds = getProjectSelection(); if (!ds.isEmpty()) { MouseInfo mi = mouseUtil.getMouseInfo(0); //System.out.println("LAST MOUSE INFO: " + mi); if (mi != null) mouseInfo = mi; if (inPasteMode(e)) { // Make sure that this owner part is active now. if (!hasFocus) scheduleActivateOwnerPart(); updateSG(ds); setDirty(); } else { if (hideSG(ds)) setDirty(); } } return false; } void updateSG(DiagramSelection selection) { MouseInfo mi = mouseUtil.getMouseInfo(0); if (mi == null) return; //ghostNode.setComposite(AlphaComposite.SrcAtop.derive(0.40f)); //ghostNode.setComposite(null); moveGhostElements(selection, mi.canvasPosition); if (selection.getSourceCanvas() != getContext()) { for (IElement e : selection.getOriginalElements()) { INode node = e.getHint(ElementHints.KEY_SG_NODE); //System.out.println("ghost element: " + e + ", node=" + node); if (node instanceof IG2DNode) { LocalDelegateNode delegate = getOrCreateNode(ghostNode, ElementUtils.generateNodeId(e), LocalDelegateNode.class); delegate.setDelegate( (IG2DNode) node ); } } } else { for (IElement e : selection.getOriginalElements()) { //System.out.println("ghost element: " + e); INode node = e.getHint(ElementHints.KEY_SG_NODE); if (node != null) { //System.out.println("ghost node: " + node); ghostNodeMapper.add(node); String nodeId = ghostNodeMapper.getId(node); //System.out.println("ghost node id: " + nodeId); LinkNode delegate = getOrCreateNode(ghostNode, ElementUtils.generateNodeId(e), LinkNode.class); delegate.setDelegateId( nodeId ); } } } ghostNode.setVisible(true); } private T getOrCreateNode(ParentNode parentNode, String id, Class clazz) { INode n = ghostNode.getNode(id); if (clazz.isInstance(n)) return clazz.cast(n); ghostNode.removeNode(id); return ghostNode.addNode(id, clazz); } protected boolean hasHighlight() { return highlightMode != null; } protected void removeHighlight() { if (isRemoved()) return; assert getContext().getThreadAccess().currentThreadAccess(); if (highlightMode != null) { if (!highlightMode.isRemoved()) { highlightMode.remove(); setDirty(); } highlightMode = null; } } private boolean inPasteMode(MouseEvent e) { return (e.stateMask & MouseEvent.CTRL_MASK) != 0; } protected void selectedMessage(DiagramSelection ds) { int size = ds.getOriginalElements().size(); StringBuilder sb = new StringBuilder(); if (size == 0) { sb.append("No elements to "); if (ds.isCut()) sb.append("cut"); else sb.append("copy"); } else { if (ds.isCut()) sb.append("Cut "); else sb.append("Copied "); sb.append(size); sb.append(" element"); if (size > 1) sb.append('s'); } message(sb.toString()); } protected void message(final String message) { if (statusLine == null) return; swtExec(new Runnable() { @Override public void run() { statusLine.setMessage(message); statusLine.setErrorMessage(null); } }); } protected void error(final String message) { if (statusLine == null) return; swtExec(new Runnable() { @Override public void run() { statusLine.setErrorMessage(message); } }); } protected void swtExec(Runnable r) { ThreadUtils.asyncExec(SWTThread.getThreadAccess(Display.getDefault()), r); } // MONITOR PASTE SUPPORT private boolean tryPasteMonitors() { SimanticsClipboard clipboard = Simantics.getClipboard(); for (Set content : clipboard.getContents()) { try { final Variable var_ = ClipboardUtils.accept(content, SimanticsKeys.KEY_VARIABLE); if (var_ != null) { final Resource diagramResource = diagram.getHint(DiagramModelHints.KEY_DIAGRAM_RESOURCE); final Resource runtime = diagram.getHint(DiagramModelHints.KEY_DIAGRAM_RUNTIME_RESOURCE); final Resource elementResource = Simantics.getSession().syncRequest(new Read() { @Override public Resource perform(ReadGraph graph) throws DatabaseException { DiagramResource DIA = DiagramResource.getInstance(graph); String diagramVariable = graph.getPossibleRelatedValue(runtime, DIA.RuntimeDiagram_HasVariable); if (diagramVariable == null) return null; Variable diaVar = Variables.getPossibleVariable(graph, diagramVariable); if (diaVar == null) return null; Variable var = Variables.switchRealization(graph, var_, Variables.getRealization(graph, diaVar)); if(var == null) return null; Variable component = Variables.getChild(graph, diaVar, var); if (component == null) return null; Resource componentResource = component.getPossibleRepresents(graph); if (componentResource == null) return null; return graph.getPossibleObject(componentResource, ModelingResources.getInstance(graph).ComponentToElement); } }); if (elementResource == null) return false; final AffineTransform monitorTransform = Simantics.getSession().syncRequest(new Read() { @Override public AffineTransform perform(ReadGraph graph) throws DatabaseException { AffineTransform at = null; if (graph.isInstanceOf(elementResource, DiagramResource.getInstance(graph).Connection)) { Resource tailNode = ConnectionUtil.getConnectionTailNode(graph, elementResource); if (tailNode != null) { at = DiagramGraphUtil.getAffineTransform(graph, tailNode); } } if (at == null) at = DiagramGraphUtil.getAffineTransform(graph, elementResource); return at; } }); MouseInfo mi = mouseUtil.getMouseInfo(0); if (mi == null) mi = mouseInfo; final double dx = mi.canvasPosition.getX() - monitorTransform.getTranslateX(); final double dy = mi.canvasPosition.getY() - monitorTransform.getTranslateY(); Simantics.getSession().asyncRequest(new WriteRequest() { @Override public void perform(WriteGraph graph) throws DatabaseException { Layer0 L0 = Layer0.getInstance(graph); Layer0X L0X = Layer0X.getInstance(graph); DiagramResource DIA = DiagramResource.getInstance(graph); G2DResource G2D = G2DResource.getInstance(graph); String diagramVariable = graph.getPossibleRelatedValue(runtime, DIA.RuntimeDiagram_HasVariable); if (diagramVariable == null) return; Variable diaVar = Variables.getPossibleVariable(graph, diagramVariable); if (diaVar == null) return; Variable var = Variables.switchRealization(graph, var_, Variables.getRealization(graph, diaVar)); if(var == null) return; Variable component = Variables.getChild(graph, diaVar, var); if (component == null) return; Resource componentResource = component.getPossibleRepresents(graph); if (componentResource == null) return; String suffix = Variables.getRVI(graph, component, var); Resource resource = graph.newResource(); graph.claim(resource, L0.InstanceOf, null, DIA.Monitor); final double scale = monitorScale; DiagramGraphUtil.setTransform(graph, resource, new AffineTransform(scale, 0, 0, scale, dx, dy)); OrderedSetUtils.add(graph, diagramResource, resource); // 5.1. Give running name to element and increment the counter attached to the diagram. Long l = graph.getPossibleRelatedValue(diagramResource, DIA.HasModCount, Bindings.LONG); if (l == null) l = Long.valueOf(0L); graph.claimLiteral(resource, L0.HasName, l.toString(), Bindings.STRING); graph.claimLiteral(diagramResource, DIA.HasModCount, ++l, Bindings.LONG); // 5.2. Make the diagram consist of the new element graph.claim(diagramResource, L0.ConsistsOf, resource); graph.claim(resource, G2D.HasHorizontalAlignment, null, G2D.Alignment_Leading); graph.claimLiteral(resource, DIA.HasDirection, 0.0); graph.claim(resource, DIA.HasMonitorComponent, componentResource); graph.claimLiteral(resource, DIA.HasMonitorSuffix, suffix); Resource model = Variables.getModel(graph, diaVar); if (model != null) { Resource template = graph.getPossibleObject(model, DIA.HasDefaultMonitorTemplate); if (template != null) { graph.claim(resource, L0X.ObtainsProperty1, null, template); } } } }); } } catch (DatabaseException e1) { Logger.defaultLogError(e1); } } return true; } }