package com.horstmann.violet.workspace.editorpart.behavior;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoableEdit;
import com.horstmann.violet.framework.file.GraphFile;
import com.horstmann.violet.framework.file.IGraphFile;
import com.horstmann.violet.framework.file.persistence.IFilePersistenceService;
import com.horstmann.violet.framework.file.persistence.XStreamBasedPersistenceService;
import com.horstmann.violet.framework.injection.bean.ManiocFramework.BeanInjector;
import com.horstmann.violet.product.diagram.abstracts.IGraph;
import com.horstmann.violet.product.diagram.abstracts.Id;
import com.horstmann.violet.product.diagram.abstracts.edge.IEdge;
import com.horstmann.violet.product.diagram.abstracts.node.INode;
import com.horstmann.violet.workspace.editorpart.IEditorPart;
import com.horstmann.violet.workspace.editorpart.IEditorPartBehaviorManager;
import com.horstmann.violet.workspace.editorpart.IEditorPartSelectionHandler;
public class CutCopyPasteBehavior extends AbstractEditorPartBehavior
{
/**
* The concerned workspace
*/
private IEditorPart editorPart;
/**
* Used to convert graph to XML and to get graph back from XML
*/
private IFilePersistenceService persistenceService = new XStreamBasedPersistenceService();
/**
* Keep mouse location to paste on just above the current mouse location
*/
private Point2D lastMouseLocation = new Point2D.Double(0, 0);
/**
* Default constructor
*
* @param editorPart
*/
public CutCopyPasteBehavior(IEditorPart editorPart)
{
BeanInjector.getInjector().inject(this);
this.editorPart = editorPart;
}
@Override
public void onMousePressed(MouseEvent event)
{
double zoom = editorPart.getZoomFactor();
this.lastMouseLocation = new Point2D.Double(event.getX() / zoom, event.getY() / zoom);
}
/**
* Cuts selected graph elements
*/
public void cut()
{
copy();
editorPart.removeSelected();
editorPart.getSwingComponent().invalidate();
editorPart.getSwingComponent().repaint();
}
/**
* Copies selected graph elements to system clipboard
*/
public void copy()
{
IGraph graph = editorPart.getGraph();
Class<? extends IGraph> graphClass = graph.getClass();
IGraphFile newGraphFile = new GraphFile(graphClass);
IGraph newGraph = newGraphFile.getGraph();
IEditorPartSelectionHandler selectionHandler = editorPart.getSelectionHandler();
List<INode> selectedNodes = selectionHandler.getSelectedNodes();
// We use an mapper for ids because they are changed when each node_old is added to the newGraph
Map<Id, Id> idMapper = new HashMap<Id, Id>();
for (INode aSelectedNode : selectedNodes)
{
if (isAncestorInCollection(aSelectedNode, selectedNodes)) {
// In this case, id doesn't change. It only changes on newGraph.addNode()
Id currentId = aSelectedNode.getId();
idMapper.put(currentId, currentId);
continue;
}
INode clone = aSelectedNode.clone();
Id oldId = clone.getId();
Point2D locationOnGraph = aSelectedNode.getLocationOnGraph();
newGraph.addNode(clone, locationOnGraph);
Id newId = clone.getId();
idMapper.put(oldId, newId);
}
List<IEdge> selectedEdges = selectionHandler.getSelectedEdges();
for (IEdge aSelectedEdge : selectedEdges)
{
IEdge clone = aSelectedEdge.clone();
Point2D startLocation = clone.getStartLocation();
Point2D endLocation = clone.getEndLocation();
Point2D[] transitionPoints = clone.getTransitionPoints();
Id oldStartId = clone.getStartNode().getId();
Id oldEndId = clone.getEndNode().getId();
Id newStartId = idMapper.get(oldStartId);
Id newEndId = idMapper.get(oldEndId);
INode startNode = newGraph.findNode(newStartId);
INode endNode = newGraph.findNode(newEndId);
if (startNode != null && endNode != null)
{
newGraph.connect(clone, startNode, startLocation, endNode, endLocation, transitionPoints);
}
}
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
persistenceService.write(newGraph, byteArrayOutputStream);
byteArrayOutputStream.toString();
String xmlContent = byteArrayOutputStream.toString();
pushContentToSystemClipboard(xmlContent);
}
/**
* Paste elements from system wide clipboard to graph
*/
public void paste()
{
IGraph graph = this.editorPart.getGraph();
try
{
String xmlContent = getContentFromSystemClipboard();
if (xmlContent == null)
{
return; // If no content, we stop here
}
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xmlContent.getBytes());
IGraph deserializedGraph = persistenceService.read(byteArrayInputStream);
deserializedGraph = translateToMouseLocation(deserializedGraph, this.lastMouseLocation);
Collection<INode> nodesFromClipboard = deserializedGraph.getAllNodes();
List<INode> nodes = filterOnNodePrototypes(nodesFromClipboard);
List<INode> nodesReallyPasted = new ArrayList<INode>();
for (INode aNode : nodes)
{
if (isAncestorInCollection(aNode, nodes)) continue;
boolean isAdded = graph.addNode(aNode, aNode.getLocationOnGraph());
if (isAdded)
{
nodesReallyPasted.add(aNode);
}
}
Collection<IEdge> edgesFromClipboard = deserializedGraph.getAllEdges();
List<IEdge> edges = filterOnEdgePrototypes(edgesFromClipboard);
List<IEdge> edgesReallyPasted = new ArrayList<IEdge>();
for (IEdge anEdge : edges)
{
Point2D startLocation = anEdge.getStartLocation();
Point2D endLocation = anEdge.getEndLocation();
Point2D[] transitionPoints = anEdge.getTransitionPoints();
INode startNode = graph.findNode(anEdge.getStartNode().getId());
INode endNode = graph.findNode(anEdge.getEndNode().getId());
if (startNode != null && endNode != null)
{
boolean isConnected = graph.connect(anEdge, startNode, startLocation, endNode, endLocation, transitionPoints);
if (isConnected)
{
edgesReallyPasted.add(anEdge);
}
}
}
addUndoRedoSupport(nodesReallyPasted, edgesReallyPasted);
selectPastedElements(nodesReallyPasted, edgesReallyPasted);
editorPart.getSwingComponent().invalidate();
editorPart.getSwingComponent().repaint();
}
catch (IOException e)
{
// Nothing to do
}
}
/**
* Adds Undo/Redo support to copy pastes
*
* @param nodesPasted
* @param edgesPasted
*/
private void addUndoRedoSupport(List<INode> nodesPasted, List<IEdge> edgesPasted)
{
IEditorPartBehaviorManager behaviorManager = this.editorPart.getBehaviorManager();
List<UndoRedoCompoundBehavior> found = behaviorManager.getBehaviors(UndoRedoCompoundBehavior.class);
if (found.size() != 1)
{
return;
}
UndoRedoCompoundBehavior undoRedoBehavior = found.get(0);
undoRedoBehavior.startHistoryCapture();
CompoundEdit capturedEdit = undoRedoBehavior.getCurrentCapturedEdit();
for (final INode aNode : nodesPasted)
{
UndoableEdit edit = new AbstractUndoableEdit()
{
@Override
public void undo() throws CannotUndoException
{
IGraph graph = editorPart.getGraph();
graph.removeNode(aNode);
super.undo();
}
@Override
public void redo() throws CannotRedoException
{
super.redo();
IGraph graph = editorPart.getGraph();
graph.addNode(aNode, aNode.getLocationOnGraph());
}
};
capturedEdit.addEdit(edit);
}
for (final IEdge anEdge : edgesPasted)
{
UndoableEdit edit = new AbstractUndoableEdit()
{
@Override
public void undo() throws CannotUndoException
{
IGraph graph = editorPart.getGraph();
graph.removeEdge(anEdge);
super.undo();
}
@Override
public void redo() throws CannotRedoException
{
super.redo();
IGraph graph = editorPart.getGraph();
graph.connect(anEdge, anEdge.getStartNode(), anEdge.getStartLocation(), anEdge.getEndNode(), anEdge.getEndLocation(), anEdge.getTransitionPoints());
}
};
capturedEdit.addEdit(edit);
}
undoRedoBehavior.stopHistoryCapture();
}
private void selectPastedElements(List<INode> nodesPasted, List<IEdge> edgesPasted)
{
IEditorPartSelectionHandler selectionHandler = this.editorPart.getSelectionHandler();
selectionHandler.clearSelection();
for (final INode aNode : nodesPasted)
{
selectionHandler.addSelectedElement(aNode);
}
for (final IEdge anEdge : edgesPasted)
{
selectionHandler.addSelectedElement(anEdge);
}
}
/**
* As we can copy/paste on many diagrams, we ensure that we paste only node_old types acceptable for the current diagram
*
* @param nodes from clipboard
* @return node acceptable for the current diagram
*/
private List<INode> filterOnNodePrototypes(Collection<INode> nodes)
{
IGraph currentGraph = this.editorPart.getGraph();
List<INode> nodePrototypes = currentGraph.getNodePrototypes();
List<Class<? extends INode>> classPrototypes = new ArrayList<Class<? extends INode>>();
for (INode aNodePrototype : nodePrototypes)
{
classPrototypes.add(aNodePrototype.getClass());
}
List<INode> result = new ArrayList<INode>();
for (INode aNode : nodes)
{
Class<? extends INode> nodeClass = aNode.getClass();
if (classPrototypes.contains(nodeClass))
{
result.add(aNode);
}
}
return result;
}
/**
* As we can copy/paste on many diagrams, we ensure that we paste only edge types acceptable for the current diagram
*
* @param edges from clipboard
* @return edges acceptable for the current diagram
*/
private List<IEdge> filterOnEdgePrototypes(Collection<IEdge> edges)
{
IGraph currentGraph = this.editorPart.getGraph();
List<IEdge> edgePrototypes = currentGraph.getEdgePrototypes();
List<Class<? extends IEdge>> classPrototypes = new ArrayList<Class<? extends IEdge>>();
for (IEdge aEdgePrototype : edgePrototypes)
{
classPrototypes.add(aEdgePrototype.getClass());
}
List<IEdge> result = new ArrayList<IEdge>();
for (IEdge aEdge : edges)
{
Class<? extends IEdge> nodeClass = aEdge.getClass();
if (classPrototypes.contains(nodeClass))
{
result.add(aEdge);
}
}
return result;
}
/**
* Moves all the node of a graph to a location
*
* @param graph
* @param mouseLocation
* @return the modified graph
*/
private IGraph translateToMouseLocation(IGraph graph, Point2D mouseLocation)
{
Rectangle2D clipBounds = graph.getClipBounds();
double dx = mouseLocation.getX() - clipBounds.getX();
double dy = mouseLocation.getY() - clipBounds.getY();
Collection<INode> nodes = graph.getAllNodes();
for (INode aNode : nodes)
{
boolean hasParent = (aNode.getParent() != null);
if (!hasParent)
{
aNode.translate(dx, dy);
}
}
return graph;
}
/**
* Deals with system wide clipboard
*
* @param content
*/
private void pushContentToSystemClipboard(String content)
{
StringSelection dataToClip = new StringSelection(content);
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(dataToClip, dataToClip);
}
/**
* Deals with system wide clipboard
*
* @return
*/
private String getContentFromSystemClipboard()
{
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
Transferable clipData = clipboard.getContents(clipboard);
if (clipData != null)
{
try
{
if (clipData.isDataFlavorSupported(DataFlavor.stringFlavor))
{
String s = (String) (clipData.getTransferData(DataFlavor.stringFlavor));
return s;
}
}
catch (UnsupportedFlavorException ufe)
{
return null;
}
catch (IOException ioe)
{
return null;
}
}
return null;
}
/**
* Converts a string into a bytebuffer
*
* @param msg
* @return
*/
private ByteBuffer convertToByteBuffer(String msg)
{
return ByteBuffer.wrap(msg.getBytes());
}
/**
* Converts a bytebuffer into a string
*
* @param bytebuffer
* @return
*/
private String convertToString(ByteBuffer bytebuffer)
{
byte[] bytearray = new byte[bytebuffer.remaining()];
bytebuffer.get(bytearray);
String s = new String(bytearray);
return s;
}
/**
* Checks if the given list contains an ancestor of the given node_old
*
* @param childNode
* @param ancestorList
* @return b
*/
private boolean isAncestorInCollection(INode childNode, Collection<INode> ancestorList)
{
for (INode anAncestorNode : ancestorList)
{
boolean ancestorRelationship = isAncestorRelationship(childNode, anAncestorNode);
if (ancestorRelationship) return true;
}
return false;
}
/**
* Checks if ancestorNode is a parent node_old of child node_old
*
* @param childNode
* @param ancestorNode
* @return b
*/
private boolean isAncestorRelationship(INode childNode, INode ancestorNode)
{
INode parent = childNode.getParent();
if (parent == null)
{
return false;
}
List<INode> fifo = new ArrayList<INode>();
fifo.add(parent);
while (!fifo.isEmpty())
{
INode aParentNode = fifo.get(0);
fifo.remove(0);
if (aParentNode.equals(ancestorNode))
{
return true;
}
INode aGranParent = aParentNode.getParent();
if (aGranParent != null)
{
fifo.add(aGranParent);
}
}
return false;
}
private List<INode> getFamily(INode aParentNode) {
List<INode> family = new ArrayList<INode>();
List<INode> fifo = new ArrayList<INode>();
fifo.addAll(aParentNode.getChildren());
while (!fifo.isEmpty()) {
INode aFamilyMember = fifo.get(0);
family.add(aFamilyMember);
fifo.addAll(aFamilyMember.getChildren());
fifo.remove(0);
}
return family;
}
}