Mercurial > hg > ccmieditor
view java/src/uk/ac/qmul/eecs/ccmi/gui/GraphPanel.java @ 0:9418ab7b7f3f
Initial import
author | Fiore Martin <fiore@eecs.qmul.ac.uk> |
---|---|
date | Fri, 16 Dec 2011 17:35:51 +0000 |
parents | |
children | 9e67171477bc |
line wrap: on
line source
/* CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package uk.ac.qmul.eecs.ccmi.gui; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.ResourceBundle; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.KeyStroke; import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionEvent; import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionListener; import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionModel; import uk.ac.qmul.eecs.ccmi.diagrammodel.ConnectNodesException; import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModelTreeNode; import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramNode; import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent; import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; /** * A panel to draw a graph */ @SuppressWarnings("serial") public class GraphPanel extends JPanel{ /** * Constructs a graph. * @param aDiagram a diagram to paint in the graph * @param a aToolbar a toolbar containing the node and edges prototypes for creating * elements in the graph. */ public GraphPanel(Diagram aDiagram, GraphToolbar aToolbar) { grid = new Grid(); gridSize = GRID; grid.setGrid((int) gridSize, (int) gridSize); zoom = 1; toolbar = aToolbar; setBackground(Color.WHITE); wasMoving = false; minBounds = null; this.model = aDiagram.getCollectionModel(); synchronized(model.getMonitor()){ edges = new LinkedList<Edge>(model.getEdges()); nodes = new LinkedList<Node>(model.getNodes()); } setModelUpdater(aDiagram.getModelUpdater()); selectedElements = new HashSet<DiagramElement>(); moveLockedElements = new HashSet<Object>(); toolbar.addEdgeCreatedListener(new innerEdgeListener()); getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE,0),"delete"); getActionMap().put("delete", new AbstractAction(){ @Override public void actionPerformed(ActionEvent evt) { /* nothing selected DELETE key has no effect */ if(selectedElements.isEmpty()) return; /* create a new Set to maintain iterator consistency as elementTakenOut will change selectedItems */ HashSet<DiagramElement> iterationSet = new HashSet<DiagramElement>(selectedElements); HashSet<DiagramElement>alreadyLockedElements = new HashSet<DiagramElement>(); /* check which, of the selected elements, can be deleted and which ones are currently held by * * other clients. If an element is locked it's removed from the list and put into a separated set */ for(Iterator<DiagramElement> itr=iterationSet.iterator(); itr.hasNext();){ DiagramElement selected = itr.next(); if(!modelUpdater.getLock(selected, Lock.DELETE)){ itr.remove(); alreadyLockedElements.add(selected); } } ResourceBundle resources = ResourceBundle.getBundle(EditorFrame.class.getName()); /* all the elements are locked by other clients */ if(iterationSet.isEmpty()){ iLog("Could not get lock on any selected element for deletion",""); JOptionPane.showMessageDialog( JOptionPane.getFrameForComponent(GraphPanel.this), alreadyLockedElements.size() == 1 ? // singular vs plural resources.getString("dialog.lock_failure.delete") : resources.getString("dialog.lock_failure.deletes")); return; } String warning = ""; if(!alreadyLockedElements.isEmpty()){ StringBuilder builder = new StringBuilder(resources.getString("dialog.lock_failure.deletes_warning")); for(DiagramElement alreadyLocked : alreadyLockedElements) builder.append(alreadyLocked.getName()).append(' '); warning = builder.append('\n').toString(); iLog("Could not get lock on some selected element for deletion",warning); } iLog("open delete dialog",warning); int answer = JOptionPane.showConfirmDialog( JOptionPane.getFrameForComponent(GraphPanel.this), warning+resources.getString("dialog.confirm.deletions"), resources.getString("dialog.confirm.title"), SpeechOptionPane.YES_NO_OPTION); if(answer == JOptionPane.YES_OPTION){ /* the user chose to delete the elements, proceed (locks * * will be automatically removed upon deletion by the server ) */ for(DiagramElement selected : iterationSet) modelUpdater.takeOutFromCollection(selected); }else{ /* the user chose not to delete the elements, release the acquired locks */ for(DiagramElement selected : iterationSet){ /* if it's a node all its attached edges were locked as well */ /*if(selected instanceof Node){ DONE IN THE SERVER Node n = (Node)selected; for(int i=0; i<n.getEdgesNum();i++){ modelUpdater.yieldLock(n.getEdgeAt(i), Lock.DELETE); } }*/ modelUpdater.yieldLock(selected, Lock.DELETE); } iLog("cancel delete node dialog",""); } }}); /* ---- COLLECTION LISTENER ---- * Adding a collection listener. This listener reacts at changes in the model * by any source, and thus the graph itself. Basically it refreshes the graph * and paints again all the nodes and edges. */ model.addCollectionListener(new CollectionListener(){ @Override public void elementInserted(final CollectionEvent e) { DiagramElement element = e.getDiagramElement(); if(element instanceof Node) nodes.add((Node)element); else edges.add((Edge)element); checkBounds(element,false); if(e.getDiagramElement() instanceof Node && e.getSource().equals(model) ){ //FIXME change model into this model source changes setElementSelected(e.getDiagramElement()); dragMode = DRAG_NODE; } revalidate(); repaint(); } @Override public void elementTakenOut(final CollectionEvent e) { DiagramElement element = e.getDiagramElement(); if(element instanceof Node){ if(nodePopup != null && nodePopup.nodeRef.equals(element)) nodePopup.setVisible(false); nodes.remove(element); } else{ if(edgePopup != null && edgePopup.edgeRef.equals(element)) edgePopup.setVisible(false); edges.remove(element); } checkBounds(e.getDiagramElement(),true); removeElementFromSelection(e.getDiagramElement()); revalidate(); repaint(); } @Override public void elementChanged(final ElementChangedEvent e) { /* we changed the position of an element and might need to update the boundaries */ if(e.getChangeType().equals("stop_move")){ checkBounds(e.getDiagramElement(),false); } revalidate(); repaint(); } }); /* --------------------------------------------------------------------------- */ /* ------------- MOUSE LISTENERS -------------------------------------------- * For pressed and released mouse click and moved mouse */ addMouseListener(new MouseAdapter(){ @Override public void mousePressed(MouseEvent event){ requestFocusInWindow(); final Point2D mousePoint = new Point2D.Double( (event.getX()+minX)/zoom, (event.getY()+minY)/zoom ); boolean isCtrl = (event.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0; Node n = Finder.findNode(mousePoint,nodes); Edge e = Finder.findEdge(mousePoint,edges); Object tool = toolbar.getSelectedTool(); /* - right click - */ if((event.getModifiers() & InputEvent.BUTTON1_MASK) == 0) { if(e != null){ if( e.contains(mousePoint)){ Node extremityNode = e.getClosestNode(mousePoint,EDGE_END_MIN_CLICK_DIST); if(extremityNode == null){ // click far from the attached nodes, only prompt with set name item EdgePopupMenu pop = new EdgePopupMenu(e,GraphPanel.this,modelUpdater); edgePopup = pop; pop.show(GraphPanel.this, event.getX(), event.getY()); }else{ // click near an attached nodes, prompt for name change, set end label and select arrow head EdgePopupMenu pop = new EdgePopupMenu(e,extremityNode,GraphPanel.this,modelUpdater); edgePopup = pop; pop.show(GraphPanel.this, event.getX(), event.getY()); } } }else if(n != null){ NodePopupMenu pop = new NodePopupMenu(n,GraphPanel.this,modelUpdater); nodePopup = pop; pop.show(GraphPanel.this, event.getX(), event.getY()); }else return; } /* - one click && palette == select - */ else if (tool == null){ if(n != null){ // node selected if (isCtrl) addElementToSelection(n,false); else setElementSelected(n); dragMode = DRAG_NODE; }else if (e != null){ // edge selected if (isCtrl){ addElementToSelection(e,false); dragMode = DRAG_NODE; }else{ setElementSelected(e); modelUpdater.startMove(e, mousePoint); dragMode = DRAG_EDGE; } }else{ // nothing selected : make selection lasso if (!isCtrl) clearSelection(); dragMode = DRAG_LASSO; } } /* - one click && palette == node - */ else { /* click on an already existing node = select it*/ if (n != null){ if (isCtrl) addElementToSelection(n,false); else setElementSelected(n); dragMode = DRAG_NODE; }else{ Node prototype = (Node) tool; Node newNode = (Node) prototype.clone(); Rectangle2D bounds = newNode.getBounds(); /* perform the translation from the origin */ newNode.translate(new Point2D.Double(), mousePoint.getX() - bounds.getX(), mousePoint.getY() - bounds.getY()); /* log stuff */ iLog("insert node",""+((newNode.getId() == DiagramElement.NO_ID) ? "(no id)" : newNode.getId())); /* insert the node into the model (no lock needed) */ modelUpdater.insertInCollection(newNode); } } lastMousePoint = mousePoint; mouseDownPoint = mousePoint; repaint(); } @Override public void mouseReleased(MouseEvent event){ final Point2D mousePoint = new Point2D.Double( (event.getX()+minX)/zoom, (event.getY()+minY)/zoom ); if(lastSelected != null){ if(lastSelected instanceof Node){ if(wasMoving){ iLog("move selected stop",mousePoint.getX()+" "+ mousePoint.getY()); for(Object element : moveLockedElements){ modelUpdater.stopMove((GraphElement)element); modelUpdater.yieldLock((DiagramModelTreeNode)element, Lock.MOVE); } moveLockedElements.clear(); } }else{ // instanceof Edge if(wasMoving){ iLog("bend edge stop",mousePoint.getX()+" "+ mousePoint.getY()); if(moveLockedEdge != null){ modelUpdater.stopMove(moveLockedEdge); modelUpdater.yieldLock(moveLockedEdge, Lock.MOVE); moveLockedEdge = null; } } } } dragMode = DRAG_NONE; wasMoving = false; repaint(); } }); addMouseMotionListener(new MouseMotionAdapter(){ public void mouseDragged(MouseEvent event){ Point2D mousePoint = new Point2D.Double( (event.getX()+minX)/zoom, (event.getY()+minY)/zoom ); boolean isCtrl = (event.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0; if (dragMode == DRAG_NODE){ /* translate selected nodes (edges as well) */ double dx = mousePoint.getX() - lastMousePoint.getX(); double dy = mousePoint.getY() - lastMousePoint.getY(); if(!wasMoving){ wasMoving = true; /* when the motion starts, we need to get the move-lock from the server */ Iterator<DiagramElement> iterator = selectedElements.iterator(); while(iterator.hasNext()){ DiagramElement element = iterator.next(); if(modelUpdater.getLock(element, Lock.MOVE)){ moveLockedElements.add(element); }else{ iLog("Could not get move lock for element",DiagramElement.toLogString(element)); iterator.remove(); } } iLog("move selected start",mousePoint.getX()+" "+ mousePoint.getY()); } for (DiagramElement selected : selectedElements){ if(selected instanceof Node) modelUpdater.translate((Node)selected, lastMousePoint, dx, dy); else modelUpdater.translate((Edge)selected, lastMousePoint, dx, dy); } } else if(dragMode == DRAG_EDGE){ if(!wasMoving){ wasMoving = true; if(modelUpdater.getLock(lastSelected, Lock.MOVE)) moveLockedEdge = (Edge)lastSelected; else iLog("Could not get move lock for element",DiagramElement.toLogString(lastSelected)); iLog("bend edge start",mousePoint.getX()+" "+ mousePoint.getY()); } if(moveLockedEdge != null) modelUpdater.bend(moveLockedEdge, new Point2D.Double(mousePoint.getX(), mousePoint.getY())); } else if (dragMode == DRAG_LASSO){ double x1 = mouseDownPoint.getX(); double y1 = mouseDownPoint.getY(); double x2 = mousePoint.getX(); double y2 = mousePoint.getY(); Rectangle2D.Double lasso = new Rectangle2D.Double(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x1 - x2), Math.abs(y1 - y2)); for (Node n : GraphPanel.this.nodes){ Rectangle2D bounds = n.getBounds(); if(!isCtrl && !lasso.contains(bounds)){ removeElementFromSelection(n); } else if (lasso.contains(bounds)){ addElementToSelection(n,true); } } if(selectedElements.size() != oldLazoSelectedNum){ StringBuilder builder = new StringBuilder(); for(DiagramElement de : selectedElements) builder.append(DiagramElement.toLogString(de)).append(' '); iLog("added by lazo",builder.toString()); } oldLazoSelectedNum = selectedElements.size(); } lastMousePoint = mousePoint; } }); } /* --------------------------------------------------------------------------- */ @Override public void paintComponent(Graphics g){ super.paintComponent(g); paintGraph(g); } public void paintGraph(Graphics g){ Graphics2D g2 = (Graphics2D) g; g2.translate(-minX, -minY); g2.scale(zoom, zoom); Rectangle2D bounds = getBounds(); Rectangle2D graphBounds = getGraphBounds(); if (!hideGrid) grid.draw(g2, new Rectangle2D.Double(minX, minY, Math.max(bounds.getMaxX() / zoom, graphBounds.getMaxX()), Math.max(bounds.getMaxY() / zoom, graphBounds.getMaxY()))); /* draw nodes and edges */ for (Edge e : edges) e.draw(g2); for (Node n : nodes) n.draw(g2); for(DiagramElement selected : selectedElements){ if (selected instanceof Node){ Rectangle2D grabberBounds = ((Node) selected).getBounds(); drawGrabber(g2, grabberBounds.getMinX(), grabberBounds.getMinY()); drawGrabber(g2, grabberBounds.getMinX(), grabberBounds.getMaxY()); drawGrabber(g2, grabberBounds.getMaxX(), grabberBounds.getMinY()); drawGrabber(g2, grabberBounds.getMaxX(), grabberBounds.getMaxY()); } else if (selected instanceof Edge){ for(Point2D p : ((Edge)selected).getConnectionPoints()) drawGrabber(g2, p.getX(), p.getY()); } } if (dragMode == DRAG_LASSO){ Color oldColor = g2.getColor(); g2.setColor(GRABBER_COLOR); double x1 = mouseDownPoint.getX(); double y1 = mouseDownPoint.getY(); double x2 = lastMousePoint.getX(); double y2 = lastMousePoint.getY(); Rectangle2D.Double lasso = new Rectangle2D.Double(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x1 - x2) , Math.abs(y1 - y2)); g2.draw(lasso); g2.setColor(oldColor); repaint(); } } /** * Draws a single "grabber", a filled square * @param g2 the graphics context * @param x the x coordinate of the center of the grabber * @param y the y coordinate of the center of the grabber */ static void drawGrabber(Graphics2D g2, double x, double y){ final int SIZE = 5; Color oldColor = g2.getColor(); g2.setColor(GRABBER_COLOR); g2.fill(new Rectangle2D.Double(x - SIZE / 2, y - SIZE / 2, SIZE, SIZE)); g2.setColor(oldColor); } @Override public Dimension getPreferredSize(){ Rectangle2D graphBounds = getGraphBounds(); return new Dimension((int) (zoom * graphBounds.getMaxX()), (int) (zoom * graphBounds.getMaxY())); } /** * Changes the zoom of this panel. The zoom is 1 by default and is multiplied * by sqrt(2) for each positive stem or divided by sqrt(2) for each negative * step. * @param steps the number of steps by which to change the zoom. A positive * value zooms in, a negative value zooms out. */ public void changeZoom(int steps){ final double FACTOR = Math.sqrt(2); for (int i = 1; i <= steps; i++) zoom *= FACTOR; for (int i = 1; i <= -steps; i++) zoom /= FACTOR; revalidate(); repaint(); } /** * Changes the grid size of this panel. The zoom is 10 by default and is * multiplied by sqrt(2) for each positive stem or divided by sqrt(2) for * each negative step. * @param steps the number of steps by which to change the zoom. A positive * value zooms in, a negative value zooms out. */ public void changeGridSize(int steps){ final double FACTOR = Math.sqrt(2); for (int i = 1; i <= steps; i++) gridSize *= FACTOR; for (int i = 1; i <= -steps; i++) gridSize /= FACTOR; grid.setGrid((int) gridSize, (int) gridSize); repaint(); } private void addElementToSelection(DiagramElement element, boolean byLasso){ /* if not added to selected elements by including it in the lasso, the element is moved * * to the back of the collection so that it will be painted on the top on the next refresh */ if(!byLasso) if(element instanceof Node){ /* put the node in the last position so that it will be drawn on the top */ nodes.remove(element); nodes.add((Node)element); iLog("addeded node to selected",DiagramElement.toLogString(element)); }else{ /* put the edge in the last position so that it will be drawn on the top */ edges.remove(element); edges.add((Edge)element); iLog("addeded edge to selected",DiagramElement.toLogString(element)); } if(selectedElements.contains(element)){ lastSelected = element; return; } lastSelected = element; selectedElements.add(element); return; } private void removeElementFromSelection(DiagramElement element){ if (element == lastSelected){ lastSelected = null; } if(selectedElements.contains(element)){ selectedElements.remove(element); } } private void setElementSelected(DiagramElement element){ /* clear the selection */ selectedElements.clear(); lastSelected = element; selectedElements.add(element); if(element instanceof Node){ nodes.remove(element); nodes.add((Node)element); iLog("node selected",DiagramElement.toLogString(element)); }else{ edges.remove(element); edges.add((Edge)element); iLog("edge selected",DiagramElement.toLogString(element)); } } private void clearSelection(){ iLog("selection cleared",""); selectedElements.clear(); lastSelected = null; } /** * Sets the value of the hideGrid property * @param newValue true if the grid is being hidden */ public void setHideGrid(boolean newValue){ hideGrid = newValue; repaint(); } /** * Gets the value of the hideGrid property * @return true if the grid is being hidden */ public boolean getHideGrid(){ return hideGrid; } /** Gets the smallest rectangle enclosing the graph @return the bounding rectangle */ public Rectangle2D getMinBounds() { return minBounds; } public void setMinBounds(Rectangle2D newValue) { minBounds = newValue; } public Rectangle2D getGraphBounds(){ Rectangle2D r = minBounds; for (Node n : nodes){ Rectangle2D b = n.getBounds(); if (r == null) r = b; else r.add(b); } for (Edge e : edges){ r.add(e.getBounds()); } return r == null ? new Rectangle2D.Double() : new Rectangle2D.Double(r.getX(), r.getY(), r.getWidth() + Node.SHADOW_GAP + Math.abs(minX), r.getHeight() + Node.SHADOW_GAP + Math.abs(minY)); } public void setModelUpdater(DiagramModelUpdater modelUpdater){ this.modelUpdater = modelUpdater; } private void iLog(String action,String args){ InteractionLog.log("GRAPH",action,args); } private void checkBounds(DiagramElement de, boolean wasRemoved){ GraphElement ge; if(de instanceof Node) ge = (Node)de; else ge = (Edge)de; if(wasRemoved){ if(ge == top){ top = null; minY = 0; Rectangle2D bounds; for(Edge e : edges){ bounds = e.getBounds(); if(bounds.getY() < minY){ top = e; minY = bounds.getY(); } } for(Node n : nodes){ bounds = n.getBounds(); if(bounds.getY() < minY){ top = n; minY = bounds.getY(); } } } if(ge == left){ minX = 0; left = null; synchronized(model.getMonitor()){ Rectangle2D bounds; for(Edge e : model.getEdges()){ bounds = e.getBounds(); if(bounds.getX() < minX){ left = e; minX = bounds.getX(); } } for(Node n : model.getNodes()){ bounds = n.getBounds(); if(bounds.getX() < minX){ left = n; minX = bounds.getX(); } } } } }else{ // was added or translated Rectangle2D bounds = ge.getBounds(); if(top == null){ if(bounds.getY() < 0){ top = ge; minY = bounds.getY(); } }else if(ge == top){ //the top-most has been translated recalculate the new top-most, as itf it were deleted checkBounds(de, true); }else if(bounds.getY() < top.getBounds().getY()){ top = ge; minY = bounds.getY(); } if(left == null){ if(bounds.getX() < 0){ left = ge; minX = bounds.getX(); } }else if(ge == left){ checkBounds(de,true);//the left-most has been translated recalculate the new left-most, as if it were deleted } else if(bounds.getX() < left.getBounds().getX()){ left = ge; minX = bounds.getX(); } } } private class innerEdgeListener implements GraphToolbar.EdgeCreatedListener { @Override public void edgeCreated(Edge e) { ArrayList<DiagramNode> nodesToConnect = new ArrayList<DiagramNode>(selectedElements.size()); for(DiagramElement element : selectedElements){ if(element instanceof Node) nodesToConnect.add((Node)element); } try { e.connect(nodesToConnect); modelUpdater.insertInCollection(e); } catch (ConnectNodesException cnEx) { JOptionPane.showMessageDialog(GraphPanel.this, cnEx.getLocalizedMessage(), ResourceBundle.getBundle(EditorFrame.class.getName()).getString("dialog.error.title"), JOptionPane.ERROR_MESSAGE); iLog("insert edge error",cnEx.getMessage()); } } } private List<Edge> edges; private List<Node> nodes; private DiagramModelUpdater modelUpdater; private CollectionModel<Node,Edge> model; private Grid grid; private GraphToolbar toolbar; private NodePopupMenu nodePopup; private EdgePopupMenu edgePopup; private double zoom; private double gridSize; private boolean hideGrid; private boolean wasMoving; private GraphElement top; private GraphElement left; private double minX; private double minY; private DiagramElement lastSelected; private Edge moveLockedEdge; private Set<DiagramElement> selectedElements; private Set<Object> moveLockedElements; private Point2D lastMousePoint; private Point2D mouseDownPoint; private Rectangle2D minBounds; private int dragMode; private int oldLazoSelectedNum; /* button is not down, mouse motion will habe no effects */ private static final int DRAG_NONE = 0; /* one or more nodes (and eventually some edges) have been selected, mouse motion will result in a translation */ private static final int DRAG_NODE = 1; /* one edge has been selected, mouse motion will result in an edge bending */ private static final int DRAG_EDGE = 2; /* mouse button down but nothing selected, mouse motion will result in a lasso */ private static final int DRAG_LASSO = 3; // multiple selection private static final int GRID = 10; private static final double EDGE_END_MIN_CLICK_DIST = 10; public static final Color GRABBER_COLOR = new Color(0,128,255); }