/* * @(#)ConnectionTool.java * * Copyright (c) 1996-2010 The authors and contributors of JHotDraw. * You may not use, copy or modify this file, except in compliance with the * accompanying license terms. */ package org.jhotdraw.draw.tool; import javax.annotation.Nullable; import org.jhotdraw.draw.*; import org.jhotdraw.draw.connector.Connector; import javax.swing.undo.*; import org.jhotdraw.util.*; import java.awt.*; import java.awt.geom.*; import java.awt.event.*; import java.util.*; /** * A tool to create a connection between two figures. * The {@link ConnectionFigure} to be created is specified by a prototype. * The location of the start and end points are controlled by {@link Connector}s. * <p> * To create a connection using the ConnectionTool, the user does the following * mouse gestures on a DrawingView: * <ol> * <li>Press the mouse button inside of a Figure. If the ConnectionTool can * find a Connector at this location, it uses it as the starting point for * the connection.</li> * <li>Drag the mouse while keeping the mouse button pressed, and then release * the mouse button. This defines the end point of the connection. * If the ConnectionTool finds a Connector at this location, it uses it * as the end point of the connection and creates a ConnectionFigure.</li> * </ol> * <hr> * <b>Design Patterns</b> * * <p><em>Framework</em><br> * Two figures can be connected using a connection figure. The location of * the start or end point of the connection is handled by a connector object * at each connected figure.<br> * Contract: {@link org.jhotdraw.draw.Figure}, * {@link org.jhotdraw.draw.ConnectionFigure}, * {@link org.jhotdraw.draw.connector.Connector}, * {@link org.jhotdraw.draw.tool.ConnectionTool}. * * @author Werner Randelshofer * @version $Id$ */ public class ConnectionTool extends AbstractTool { private static final long serialVersionUID = 1L; /** FIXME - The ANCHOR_WIDTH value must be retrieved from the DrawingEditor */ private static final int ANCHOR_WIDTH = 6; /** * Attributes to be applied to the created ConnectionFigure. * These attributes override the default attributes of the * DrawingEditor. */ @Nullable private Map<AttributeKey<?>, Object> prototypeAttributes; /** * The Connector at the start point of the connection. */ @Nullable protected Connector startConnector; /** * The Connector at the end point of the connection. */ @Nullable protected Connector endConnector; /** * The created figure. */ @Nullable protected ConnectionFigure createdFigure; /** * the prototypical figure that is used to create new * connections. */ protected ConnectionFigure prototype; /** * The figure for which we enabled drawing of connectors. */ @Nullable protected Figure targetFigure; protected Collection<Connector> connectors = Collections.emptyList(); /** * A localized name for this tool. The presentationName is displayed by the * UndoableEdit. */ @Nullable private String presentationName; /** * If this is set to false, the CreationTool does not fire toolDone * after a new Figure has been created. This allows to create multiple * figures consecutively. */ private boolean isToolDoneAfterCreation = true; /** Creates a new instance. */ public ConnectionTool(ConnectionFigure prototype) { this(prototype, null, null); } public ConnectionTool(ConnectionFigure prototype, @Nullable Map<AttributeKey<?>, Object> attributes) { this(prototype, attributes, null); } public ConnectionTool(ConnectionFigure prototype, @Nullable Map<AttributeKey<?>, Object> attributes, @Nullable String presentationName) { this.prototype = prototype; this.prototypeAttributes = attributes; if (presentationName == null) { ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels"); presentationName = labels.getString("edit.createConnectionFigure.text"); } this.presentationName = presentationName; } public ConnectionTool(String prototypeClassName) { this(prototypeClassName, null, null); } public ConnectionTool(String prototypeClassName, @Nullable Map<AttributeKey<?>, Object> attributes, @Nullable String presentationName) { try { this.prototype = (ConnectionFigure) Class.forName(prototypeClassName).newInstance(); } catch (Exception e) { InternalError error = new InternalError("Unable to create ConnectionFigure from " + prototypeClassName); error.initCause(e); throw error; } this.prototypeAttributes = attributes; if (presentationName == null) { ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels"); presentationName = labels.getString("edit.createConnectionFigure.text"); } this.presentationName = presentationName; } public ConnectionFigure getPrototype() { return prototype; } protected int getAnchorWidth() { return ANCHOR_WIDTH; } /** * This method is called on the Figure, onto which the user wants * to start a new connection. * * @param f The ConnectionFigure. * @param startConnector The Connector of the start Figure. * @return True, if a connection can be made. */ protected boolean canConnect(ConnectionFigure f, Connector startConnector) { return f.canConnect(startConnector); } /** * This method is called on the Figure, onto which the user wants * to end a new connection. * * @param f The ConnectionFigure. * @param startConnector The Connector of the start Figure. * @param endConnector The Connector of the end Figure. * @return True, if a connection can be made. */ protected boolean canConnect(ConnectionFigure f, Connector startConnector, Connector endConnector) { return f.canConnect(startConnector, endConnector); } @Override public void mouseMoved(MouseEvent evt) { repaintConnectors(evt); } /** * Updates the list of connectors that we draw when the user * moves or drags the mouse over a figure to which can connect. */ public void repaintConnectors(MouseEvent evt) { Rectangle2D.Double invalidArea = null; Point2D.Double targetPoint = viewToDrawing(new Point(evt.getX(), evt.getY())); Figure aFigure = getDrawing().findFigureExcept(targetPoint, createdFigure); if (aFigure != null && !aFigure.isConnectable()) { aFigure = null; } if (targetFigure != aFigure) { for (Connector c : connectors) { if (invalidArea == null) { invalidArea = c.getDrawingArea(); } else { invalidArea.add(c.getDrawingArea()); } } targetFigure = aFigure; if (targetFigure != null) { connectors = targetFigure.getConnectors(getPrototype()); for (Connector c : connectors) { if (invalidArea == null) { invalidArea = c.getDrawingArea(); } else { invalidArea.add(c.getDrawingArea()); } } } } if (invalidArea != null) { getView().getComponent().repaint( getView().drawingToView(invalidArea)); } } /** * Manipulates connections in a context dependent way. If the * mouse down hits a figure start a new connection. If the mousedown * hits a connection split a segment or join two segments. */ @Override public void mousePressed(MouseEvent evt) { super.mousePressed(evt); getView().clearSelection(); Point2D.Double startPoint = viewToDrawing(anchor); Figure startFigure = getDrawing().findFigure(startPoint); startConnector = (startFigure == null) ? null : startFigure.findConnector(startPoint, prototype); if (startConnector != null && canConnect(prototype, startConnector)) { Point2D.Double anchor = startConnector.getAnchor(); createdFigure = createFigure(); createdFigure.setStartPoint(anchor); createdFigure.setEndPoint(anchor); getDrawing().add(createdFigure); Rectangle r = new Rectangle(getView().drawingToView(anchor)); r.grow(ANCHOR_WIDTH, ANCHOR_WIDTH); fireAreaInvalidated(r); } else { startConnector = null; createdFigure = null; } endConnector = null; } /** * Adjust the created connection. */ @Override public void mouseDragged(java.awt.event.MouseEvent e) { repaintConnectors(e); if (createdFigure != null) { createdFigure.willChange(); Point2D.Double endPoint = viewToDrawing(new Point(e.getX(), e.getY())); if (getView().getConstrainer()!=null) { endPoint = getView().getConstrainer().constrainPoint(endPoint, createdFigure); } Figure endFigure = getDrawing().findFigureExcept(endPoint, createdFigure); endConnector = (endFigure == null) ? null : endFigure.findConnector(endPoint, prototype); if (endConnector != null && canConnect(createdFigure, startConnector, endConnector)) { endPoint = endConnector.getAnchor(); } Rectangle r = new Rectangle(getView().drawingToView(createdFigure.getEndPoint())); createdFigure.setEndPoint(endPoint); r.add(getView().drawingToView(endPoint)); r.grow(ANCHOR_WIDTH + 2, ANCHOR_WIDTH + 2); getView().getComponent().repaint(r); createdFigure.changed(); } } /** * Connects the figures if the mouse is released over another * figure. */ @Override public void mouseReleased(MouseEvent e) { if (createdFigure != null && startConnector != null && endConnector != null && createdFigure.canConnect(startConnector, endConnector)) { createdFigure.willChange(); createdFigure.setStartConnector(startConnector); createdFigure.setEndConnector(endConnector); createdFigure.updateConnection(); createdFigure.changed(); final Figure addedFigure = createdFigure; final Drawing addedDrawing = getDrawing(); getDrawing().fireUndoableEditHappened(new AbstractUndoableEdit() { private static final long serialVersionUID = 1L; @Override public String getPresentationName() { return presentationName; } @Override public void undo() throws CannotUndoException { super.undo(); addedDrawing.remove(addedFigure); } @Override public void redo() throws CannotRedoException { super.redo(); addedDrawing.add(addedFigure); } }); targetFigure = null; Point2D.Double anchor = startConnector.getAnchor(); Rectangle r = new Rectangle(getView().drawingToView(anchor)); r.grow(ANCHOR_WIDTH, ANCHOR_WIDTH); fireAreaInvalidated(r); anchor = endConnector.getAnchor(); r = new Rectangle(getView().drawingToView(anchor)); r.grow(ANCHOR_WIDTH, ANCHOR_WIDTH); fireAreaInvalidated(r); startConnector = endConnector = null; Figure finishedFigure = createdFigure; createdFigure = null; creationFinished(finishedFigure); } else { if (isToolDoneAfterCreation()) { fireToolDone(); } } } @Override public void activate(DrawingEditor editor) { super.activate(editor); } @Override public void deactivate(DrawingEditor editor) { if (createdFigure != null) { getDrawing().remove(createdFigure); createdFigure = null; } targetFigure = null; startConnector = endConnector = null; super.deactivate(editor); } /** * Creates the ConnectionFigure. By default the figure prototype is * cloned. */ @SuppressWarnings("unchecked") protected ConnectionFigure createFigure() { ConnectionFigure f = (ConnectionFigure) prototype.clone(); getEditor().applyDefaultAttributesTo(f); if (prototypeAttributes != null) { for (Map.Entry<AttributeKey<?>, Object> entry : prototypeAttributes.entrySet()) { f.set((AttributeKey<Object>)entry.getKey(), entry.getValue()); } } return f; } @Override public void draw(Graphics2D g) { Graphics2D gg = (Graphics2D) g.create(); gg.transform(getView().getDrawingToViewTransform()); if (targetFigure != null) { for (Connector c : targetFigure.getConnectors(getPrototype())) { c.draw(gg); } } if (createdFigure != null) { createdFigure.draw(gg); Point p = getView().drawingToView(createdFigure.getStartPoint()); Ellipse2D.Double e = new Ellipse2D.Double( p.x - ANCHOR_WIDTH / 2, p.y - ANCHOR_WIDTH / 2, ANCHOR_WIDTH, ANCHOR_WIDTH); g.setColor(Color.GREEN); g.fill(e); g.setColor(Color.BLACK); g.draw(e); p = getView().drawingToView(createdFigure.getEndPoint()); e = new Ellipse2D.Double( p.x - ANCHOR_WIDTH / 2, p.y - ANCHOR_WIDTH / 2, ANCHOR_WIDTH, ANCHOR_WIDTH); g.setColor(Color.GREEN); g.fill(e); g.setColor(Color.BLACK); g.draw(e); } gg.dispose(); } /** * This method allows subclasses to do perform additonal user interactions * after the new figure has been created. * The implementation of this class just invokes fireToolDone. */ protected void creationFinished(Figure createdFigure) { if (isToolDoneAfterCreation()) { fireToolDone(); } } /** * If this is set to false, the CreationTool does not fire toolDone * after a new Figure has been created. This allows to create multiple * figures consecutively. */ public void setToolDoneAfterCreation(boolean newValue) { boolean oldValue = isToolDoneAfterCreation; isToolDoneAfterCreation = newValue; } /** * Returns true, if this tool fires toolDone immediately after a new * figure has been created. */ public boolean isToolDoneAfterCreation() { return isToolDoneAfterCreation; } }