/* Violet - A program for editing UML diagrams. Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) 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 2 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.horstmann.violet.framework; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.geom.Point2D; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.InputEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import java.awt.geom.Line2D; import java.awt.geom.Rectangle2D; import javax.swing.JPanel; import javax.swing.JOptionPane; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.ResourceBundle; import java.util.Set; /** * A panel to draw a graph */ public class GraphPanel extends JPanel { /** * Constructs a graph. * @param aToolBar the tool bar with the node and edge tools */ public GraphPanel(ToolBar aToolBar) { grid = new Grid(); gridSize = GRID; grid.setGrid((int) gridSize, (int) gridSize); zoom = 1; toolBar = aToolBar; setBackground(Color.WHITE); selectedItems = new HashSet(); addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent event) { requestFocus(); final Point2D mousePoint = new Point2D.Double(event.getX() / zoom, event.getY() / zoom); boolean isCtrl = (event.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0; Node n = graph.findNode(mousePoint); Edge e = graph.findEdge(mousePoint); Object tool = toolBar.getSelectedTool(); if (event.getClickCount() > 1 || (event.getModifiers() & InputEvent.BUTTON1_MASK) == 0) // double/right-click { if (e != null) { setSelectedItem(e); editSelected(); } else if (n != null) { setSelectedItem(n); editSelected(); } else { toolBar.showPopup(GraphPanel.this, mousePoint, new ActionListener() { public void actionPerformed(ActionEvent event) { Object tool = toolBar.getSelectedTool(); if (tool instanceof Node) { Node prototype = (Node) tool; Node newNode = (Node) prototype.clone(); boolean added = graph.add(newNode, mousePoint); if (added) { setModified(true); setSelectedItem(newNode); } } } }); } } else if (tool == null) // select { if (e != null) { setSelectedItem(e); } else if (n != null) { if (isCtrl) addSelectedItem(n); else if (!selectedItems.contains(n)) setSelectedItem(n); dragMode = DRAG_MOVE; } else { if (!isCtrl) clearSelection(); dragMode = DRAG_LASSO; } } else if (tool instanceof Node) { Node prototype = (Node) tool; Node newNode = (Node) prototype.clone(); boolean added = graph.add(newNode, mousePoint); if (added) { setModified(true); setSelectedItem(newNode); dragMode = DRAG_MOVE; } else if (n != null) { if (isCtrl) addSelectedItem(n); else if (!selectedItems.contains(n)) setSelectedItem(n); dragMode = DRAG_MOVE; } } else if (tool instanceof Edge) { if (n != null) dragMode = DRAG_RUBBERBAND; } lastMousePoint = mousePoint; mouseDownPoint = mousePoint; repaint(); } public void mouseReleased(MouseEvent event) { Point2D mousePoint = new Point2D.Double(event.getX() / zoom, event.getY() / zoom); Object tool = toolBar.getSelectedTool(); if (dragMode == DRAG_RUBBERBAND) { Edge prototype = (Edge) tool; Edge newEdge = (Edge) prototype.clone(); if (mousePoint.distance(mouseDownPoint) > CONNECT_THRESHOLD && graph.connect(newEdge, mouseDownPoint, mousePoint)) { setModified(true); setSelectedItem(newEdge); } } else if (dragMode == DRAG_MOVE) { graph.layout(); setModified(true); } dragMode = DRAG_NONE; revalidate(); repaint(); } }); addMouseMotionListener(new MouseMotionAdapter() { public void mouseDragged(MouseEvent event) { Point2D mousePoint = new Point2D.Double(event.getX() / zoom, event.getY() / zoom); boolean isCtrl = (event.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0; if (dragMode == DRAG_MOVE && lastSelected instanceof Node) { Node lastNode = (Node) lastSelected; Rectangle2D bounds = lastNode.getBounds(); double dx = mousePoint.getX() - lastMousePoint.getX(); double dy = mousePoint.getY() - lastMousePoint.getY(); // we don't want to drag nodes into negative coordinates // particularly with multiple selection, we might never be // able to get them back. Iterator iter = selectedItems.iterator(); while (iter.hasNext()) { Object selected = iter.next(); if (selected instanceof Node) { Node n = (Node) selected; bounds.add(n.getBounds()); } } dx = Math.max(dx, -bounds.getX()); dy = Math.max(dy, -bounds.getY()); iter = selectedItems.iterator(); while (iter.hasNext()) { Object selected = iter.next(); if (selected instanceof Node) { Node n = (Node) selected; n.translate(dx, dy); } } // we don't want continuous layout any more because of multiple selection // graph.layout(); } 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)); Iterator iter = graph.getNodes().iterator(); while (iter.hasNext()) { Node n = (Node) iter.next(); Rectangle2D bounds = n.getBounds(); if (!isCtrl && !lasso.contains(n.getBounds())) { removeSelectedItem(n); } else if (lasso.contains(n.getBounds())) { addSelectedItem(n); } } } lastMousePoint = mousePoint; repaint(); } }); } /** * Edits the properties of the selected graph element. */ public void editSelected() { if (EditorFrame.editProperties == true) { Object edited = lastSelected; if (lastSelected == null) { if (selectedItems.size() == 1) edited = selectedItems.iterator().next(); else return; } PropertySheet sheet = new PropertySheet(edited, this); sheet.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent event) { graph.layout(); repaint(); } }); JOptionPane.showInternalMessageDialog(this, sheet, ResourceBundle.getBundle("com.horstmann.violet.framework.EditorStrings").getString("dialog.properties"), JOptionPane.QUESTION_MESSAGE); setModified(true); } } /** * Removes the selected nodes or edges. */ public void removeSelected() { Iterator iter = selectedItems.iterator(); while (iter.hasNext()) { Object selected = iter.next(); if (selected instanceof Node) { graph.removeNode((Node) selected); } else if (selected instanceof Edge) { graph.removeEdge((Edge) selected); } } if (selectedItems.size() > 0) setModified(true); repaint(); } /** * Set the graph in the panel * @param aGraph the graph to be displayed and edited */ public void setGraph(Graph aGraph) { graph = aGraph; setModified(false); revalidate(); repaint(); } public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; g2.scale(zoom, zoom); Rectangle2D bounds = getBounds(); Rectangle2D graphBounds = graph.getBounds(g2); if (!hideGrid) grid.draw(g2, new Rectangle2D.Double(0, 0, Math.max(bounds.getMaxX() / zoom, graphBounds.getMaxX()), Math.max(bounds.getMaxY() / zoom, graphBounds.getMaxY()))); graph.draw(g2, grid); Iterator iter = selectedItems.iterator(); Set toBeRemoved = new HashSet(); while (iter.hasNext()) { Object selected = iter.next(); if (!graph.getNodes().contains(selected) && !graph.getEdges().contains(selected)) { toBeRemoved.add(selected); } else 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) { Line2D line = ((Edge) selected).getConnectionPoints(); drawGrabber(g2, line.getX1(), line.getY1()); drawGrabber(g2, line.getX2(), line.getY2()); } } iter = toBeRemoved.iterator(); while (iter.hasNext()) removeSelectedItem(iter.next()); if (dragMode == DRAG_RUBBERBAND) { Color oldColor = g2.getColor(); g2.setColor(PURPLE); g2.draw(new Line2D.Double(mouseDownPoint, lastMousePoint)); g2.setColor(oldColor); } else if (dragMode == DRAG_LASSO) { Color oldColor = g2.getColor(); g2.setColor(PURPLE); 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); } } /** * 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 */ public static void drawGrabber(Graphics2D g2, double x, double y) { final int SIZE = 5; Color oldColor = g2.getColor(); g2.setColor(PURPLE); g2.fill(new Rectangle2D.Double(x - SIZE / 2, y - SIZE / 2, SIZE, SIZE)); g2.setColor(oldColor); } public Dimension getPreferredSize() { Rectangle2D bounds = graph.getBounds((Graphics2D) getGraphics()); return new Dimension((int) (zoom * bounds.getMaxX()), (int) (zoom * bounds.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); graph.layout(); repaint(); } public void selectNext(int n) { ArrayList selectables = new ArrayList(); selectables.addAll(graph.getNodes()); selectables.addAll(graph.getEdges()); if (selectables.size() == 0) return; java.util.Collections.sort(selectables, new java.util.Comparator() { public int compare(Object obj1, Object obj2) { double x1; double y1; if (obj1 instanceof Node) { Rectangle2D bounds = ((Node) obj1).getBounds(); x1 = bounds.getX(); y1 = bounds.getY(); } else { Point2D start = ((Edge) obj1).getConnectionPoints().getP1(); x1 = start.getX(); y1 = start.getY(); } double x2; double y2; if (obj2 instanceof Node) { Rectangle2D bounds = ((Node) obj2).getBounds(); x2 = bounds.getX(); y2 = bounds.getY(); } else { Point2D start = ((Edge) obj2).getConnectionPoints().getP1(); x2 = start.getX(); y2 = start.getY(); } if (y1 < y2) return -1; if (y1 > y2) return 1; if (x1 < x2) return -1; if (x1 > x2) return 1; return 0; } }); int index; if (lastSelected == null) index = 0; else index = selectables.indexOf(lastSelected) + n; while (index < 0) index += selectables.size(); index %= selectables.size(); setSelectedItem(selectables.get(index)); repaint(); } /** * Checks whether this graph has been modified since it was last saved. * @return true if the graph has been modified */ public boolean isModified() { return modified; } /** * Sets or resets the modified flag for this graph * @param newValue true to indicate that the graph has been modified */ public void setModified(boolean newValue) { modified = newValue; if (frame == null) { Component parentComponent = this; do { parentComponent = parentComponent.getParent(); } while (parentComponent != null && !(parentComponent instanceof GraphFrame)); if (parentComponent != null) frame = (GraphFrame) parentComponent; } if (frame != null) { String title = frame.getFileName(); if (title != null) { if (modified) { if (!frame.getTitle().endsWith("*")) frame.setTitle(title + "*"); } else frame.setTitle(title); } } } private void addSelectedItem(Object obj) { lastSelected = obj; selectedItems.add(obj); } private void removeSelectedItem(Object obj) { if (obj == lastSelected) lastSelected = null; selectedItems.remove(obj); } private void setSelectedItem(Object obj) { selectedItems.clear(); lastSelected = obj; if (obj != null) selectedItems.add(obj); } private void clearSelection() { selectedItems.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; } private Graph graph; private Grid grid; private GraphFrame frame; private ToolBar toolBar; private double zoom; private double gridSize; private boolean hideGrid; private boolean modified; private Object lastSelected; private Set selectedItems; private Point2D lastMousePoint; private Point2D mouseDownPoint; private int dragMode; private static final int DRAG_NONE = 0; private static final int DRAG_MOVE = 1; private static final int DRAG_RUBBERBAND = 2; private static final int DRAG_LASSO = 3; private static final int GRID = 10; private static final int CONNECT_THRESHOLD = 8; private static final Color PURPLE = new Color(0.7f, 0.4f, 0.7f); }