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); } } }