package pipe.controllers;
import pipe.constants.GUIConstants;
import pipe.controllers.application.PipeApplicationController;
import pipe.gui.PetriNetTab;
import pipe.historyActions.MultipleEdit;
import pipe.historyActions.component.AddPetriNetObject;
import pipe.utilities.gui.GuiUtils;
import uk.ac.imperial.pipe.exceptions.PetriNetComponentException;
import uk.ac.imperial.pipe.models.petrinet.*;
import uk.ac.imperial.pipe.naming.MultipleNamer;
import uk.ac.imperial.pipe.naming.PetriNetComponentNamer;
import uk.ac.imperial.pipe.visitor.PasteVisitor;
import uk.ac.imperial.pipe.visitor.component.PetriNetComponentVisitor;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.undo.UndoableEdit;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
/**
* Class to handle copy and paste functionality
*/
@SuppressWarnings("serial")
public class CopyPasteManager extends javax.swing.JComponent
implements java.awt.event.MouseListener, java.awt.event.MouseMotionListener, java.awt.event.KeyListener {
/**
* Colour of rectangle displayed when pasting
*/
private static final Paint PASTE_COLOR = new Color(155, 155, 155, 100);
/**
* Colour of rectangle outline displayed when pasting
*/
private static final Color PASTE_COLOR_OUTLINE = new Color(155, 0, 0, 0);
/**
* Rectangle displayed which marks the outline of the objects to paste
*/
private final Rectangle pasteRectangle = new Rectangle(-1, -1);
/**
* Petri net tab where pasting takes place
*/
private final PetriNetTab petriNetTab;
/**
* Petri net pasting objects from/to
*/
private final PetriNet petriNet;
/**
* Main PIPE application controller
*/
private final PipeApplicationController applicationController;
/**
* Origin of the selected components to paste (top left corner)
*/
private final Point rectangleOrigin = new Point();
/**
* Listener for undoable events being created
*/
private final UndoableEditListener listener;
/**
* pasteInProgres is true when pasteRectangle is visible (user is doing a
* paste but still hasn't chosen the position where elements will be pasted).
*/
private boolean pasteInProgress = false;
/**
* Components to paste when paste is clicked.
* These are set when copied
*/
private Collection<PetriNetComponent> pasteComponents = new ArrayList<>();
/**
* Constructor
*
* @param listener undoable event listener, used to register undo events to
* @param petriNetTab current Petri net tab
* @param net underlying Petri net displayed on the Petri net tab
* @param applicationController main application controller
*/
public CopyPasteManager(UndoableEditListener listener, PetriNetTab petriNetTab, PetriNet net,
PipeApplicationController applicationController) {
this.petriNetTab = petriNetTab;
petriNet = net;
this.applicationController = applicationController;
addMouseListener(this);
addMouseMotionListener(this);
addKeyListener(this);
this.listener = listener;
}
/**
* Creates new components for the petri net to copy when pasted
*
* @param selectedComponents components to copy
*/
public void copy(Collection<PetriNetComponent> selectedComponents) {
pasteComponents.clear();
pasteComponents.addAll(selectedComponents);
LocationVisitor locationVisitor = new LocationVisitor();
for (PetriNetComponent component : selectedComponents) {
try {
component.accept(locationVisitor);
} catch (PetriNetComponentException e) {
GuiUtils.displayErrorMessage(null, e.getMessage());
}
}
Location location = locationVisitor.location;
pasteRectangle.setRect(location.left, location.top, location.right - location.left,
location.bottom - location.top);
rectangleOrigin.setLocation(location.left, location.top);
}
/**
* Shows the paste rectangle on screen
*/
public void showPasteRectangle() {
if (!pasteInProgress) {
petriNetTab.add(this);
requestFocusInWindow();
// if (zoom != petriNetTab.getZoom()) {
// updateSize(pasteRectangle, zoom, petriNetTab.getZoom());
// zoom = petriNetTab.getZoom();
// }
petriNetTab.setLayer(this, GUIConstants.SELECTION_LAYER_OFFSET);
repaint();
pasteInProgress = true;
updateBounds();
}
}
/**
* Update the bounds which this object can be displayed at
*/
private void updateBounds() {
if (pasteInProgress) {
PetriNetTab activeTab = applicationController.getActiveTab();
setBounds(0, 0, activeTab.getWidth(), activeTab.getHeight());
}
}
/**
* @return if it is possible to perform a paste action
*/
public boolean pasteEnabled() {
return !pasteComponents.isEmpty();
}
/**
* Paints the paste rectangle onto the screen
*
* @param g paint graphics
*/
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setPaint(PASTE_COLOR);
g2d.fill(pasteRectangle);
g2d.setXORMode(PASTE_COLOR_OUTLINE);
g2d.draw(pasteRectangle);
}
/**
* Dragging the mouse on the screen updates the location of the
* paste rectangle
*
* @param e mouse drag event
*/
@Override
public void mouseDragged(MouseEvent e) {
if (pasteInProgress) {
updateRect(e.getPoint());
}
}
/**
* Changes the rectangles location to point
*
* @param point new top left point for rectangle
*/
private void updateRect(Point point) {
pasteRectangle.setLocation(point);
repaint();
updateBounds();
}
/**
* Moving the mouse on the screen updates the location of the
* paste rectangle
*
* @param e mouse move event
*/
@Override
public void mouseMoved(MouseEvent e) {
if (pasteInProgress) {
updateRect(e.getPoint());
}
}
/**
* Noop action on click
*
* @param e mouse click event
*/
@Override
public void mouseClicked(MouseEvent e) {
//Not needed
}
/**
* Performs the paste action
*
* @param e mouse pressed event
*/
@Override
public void mousePressed(MouseEvent e) {
petriNetTab.updatePreferredSize();
petriNetTab.setLayer(this, GUIConstants.LOWEST_LAYER_OFFSET);
repaint();
if (pasteInProgress) {
paste(petriNetTab);
}
}
/**
* Noop action
*
* @param e mouse released event
*/
@Override
public void mouseReleased(MouseEvent e) {
// Not needed
}
/**
* Noop action
*
* @param e mouse entered event
*/
@Override
public void mouseEntered(MouseEvent e) {
// Not needed
}
/**
* Noop action
*
* @param e mouse exited event
*/
@Override
public void mouseExited(MouseEvent e) {
// Not needed
}
/**
* Paste pastes the new objects into the petriNet specified in consturction.
* <p>
* It first pastes the connectables, and then other components. This ordering is important
* and will ensure that arcs are created with the right components.
* </p>
* @param petriNetTab petri net tab to paste items to
*/
private void paste(PetriNetTab petriNetTab) {
pasteInProgress = false;
petriNetTab.remove(this);
if (pasteComponents.isEmpty()) {
return;
}
int despX = pasteRectangle.x - rectangleOrigin.x;
int despY = pasteRectangle.y - rectangleOrigin.y;
MultipleNamer multipleNamer = new PetriNetComponentNamer(petriNet);
PasteVisitor pasteVisitor = new PasteVisitor(petriNet, pasteComponents, multipleNamer, despX, despY);
try {
for (Connectable component : getConnectablesToPaste()) {
component.accept(pasteVisitor);
}
for (PetriNetComponent component : getNonConnectablesToPaste()) {
component.accept(pasteVisitor);
}
} catch (PetriNetComponentException e) {
GuiUtils.displayErrorMessage(null, e.getMessage());
}
createPasteHistoryItem(pasteVisitor.getCreatedComponents());
}
/**
* @return a collection of the connectable items to paste
*/
private Collection<Connectable> getConnectablesToPaste() {
final Collection<Connectable> connectables = new LinkedList<>();
PetriNetComponentVisitor connectableVisitor = new PlaceTransitionVisitor() {
@Override
public void visit(Place place) {
connectables.add(place);
}
@Override
public void visit(Transition transition) {
connectables.add(transition);
}
};
for (PetriNetComponent component : pasteComponents) {
try {
component.accept(connectableVisitor);
} catch (PetriNetComponentException e) {
GuiUtils.displayErrorMessage(null, e.getMessage());
}
}
return connectables;
}
/**
* @return Petri net components that do not inherit from Connectable
*/
private Collection<PetriNetComponent> getNonConnectablesToPaste() {
final Collection<PetriNetComponent> components = new LinkedList<>();
PetriNetComponentVisitor componentVisitor = new NonConnectableVisitor() {
@Override
public void visit(Token token) {
components.add(token);
}
@Override
public void visit(Annotation annotation) {
components.add(annotation);
}
@Override
public void visit(InboundArc inboundArc) {
components.add(inboundArc);
}
@Override
public void visit(OutboundArc outboundArc) {
components.add(outboundArc);
}
};
for (PetriNetComponent component : pasteComponents) {
try {
component.accept(componentVisitor);
} catch (PetriNetComponentException e) {
GuiUtils.displayErrorMessage(null, e.getMessage());
}
}
return components;
}
/**
* Creates a history item for the new components added to the petrinet
*
* @param createdComponents new components that have been created
*/
private void createPasteHistoryItem(Iterable<PetriNetComponent> createdComponents) {
List<UndoableEdit> undoableEditList = new LinkedList<>();
for (PetriNetComponent component : createdComponents) {
AddPetriNetObject addAction = new AddPetriNetObject(component, petriNet);
undoableEditList.add(addAction);
}
listener.undoableEditHappened(new UndoableEditEvent(this, new MultipleEdit(undoableEditList)));
}
/**
* Noop action
*
* @param e key typed event
*/
@Override
public void keyTyped(KeyEvent e) {
// Not needed
}
/**
* Noop action
*
* @param e key pressed event
*/
@Override
public void keyPressed(KeyEvent e) {
// Not needed
}
/**
* Noop action
*
* @param e key released event
*/
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
cancelPaste();
}
}
/**
* Cancel the paste. This will stop the paste rectangle being displayed but will
* keep the copied items selected for future pastes
*/
public void cancelPaste() {
PetriNetTab tab = applicationController.getActiveTab();
cancelPaste(tab);
}
/**
* Cancel the paste. This will stop the paste rectangle being displayed but will
* keep the copied items selected for future pastes
*
* @param view tab on which the paste is taking place
*/
void cancelPaste(PetriNetTab view) {
pasteInProgress = false;
view.repaint();
view.remove(this);
}
/**
* Used for creating anonymous classes that only visit
* Places and Transition
*/
private interface PlaceTransitionVisitor extends PlaceVisitor, TransitionVisitor {
}
/**
* Used for creating anonymous classes that visit non connectable classes
*/
private interface NonConnectableVisitor extends AnnotationVisitor, ArcVisitor, TokenVisitor {
}
/**
* Private class used to set the bounds of a selection rectangle
* Needed to create a class so that the visitor can change the values
*/
private static class Location {
/**
* Bottom location
*/
private double bottom = 0;
/**
* Right of the rectangle
*/
private double right = 0;
/**
* Top of the rectangle
*/
private double top = Double.MAX_VALUE;
/**
* Left of the rectangle
*/
private double left = Double.MAX_VALUE;
}
/**
* Used to set the bounds of the rectagle displayed when copy pasting
*/
private static class LocationVisitor implements PlaceTransitionVisitor {
/**
* Location of the rectangle
*/
private final Location location = new Location();
/**
* Adjusts the bounds to include the position of the place
* @param place
*/
@Override
public void visit(Place place) {
adjustLocation(place);
}
/**
* Changes the bounds of the rectangle to include the connectable
* @param connectable being bounded
* @param <T> type of the connectable
*/
private <T extends Connectable> void adjustLocation(T connectable) {
if (connectable.getX() < location.left) {
location.left = connectable.getX();
}
if (connectable.getX() + connectable.getWidth() > location.right) {
location.right = connectable.getX() + connectable.getWidth();
}
if (connectable.getY() < location.top) {
location.top = connectable.getY();
}
if (connectable.getY() + connectable.getHeight() > location.bottom) {
location.bottom = connectable.getY() + connectable.getHeight();
}
}
/**
* Adjusts the bounds of the rectangle to include the position of the transition
* @param transition to be included in the bounds
*/
@Override
public void visit(Transition transition) {
adjustLocation(transition);
}
}
}