/* $Id: FigUseCase.java 17923 2010-01-27 05:23:07Z bobtarling $ ******************************************************************************* * Copyright (c) 2009-2010 Contributors - see below * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Bob Tarling * Michiel van der Wulp ******************************************************************************* * * Some portions of this file was previously release using the BSD License: */ // $Id: FigUseCase.java 17923 2010-01-27 05:23:07Z bobtarling $ // Copyright (c) 1996-2009 The Regents of the University of California. All // Rights Reserved. Permission to use, copy, modify, and distribute this // software and its documentation without fee, and without a written // agreement is hereby granted, provided that the above copyright notice // and this paragraph appear in all copies. This software program and // documentation are copyrighted by The Regents of the University of // California. The software program and documentation are supplied "AS // IS", without any accompanying services from The Regents. The Regents // does not warrant that the operation of the program will be // uninterrupted or error-free. The end-user understands that the program // was developed for research purposes and is advised not to rely // exclusively on the program for any reason. IN NO EVENT SHALL THE // UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, // SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, // ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF // THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF // SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE // PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF // CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, // UPDATES, ENHANCEMENTS, OR MODIFICATIONS. package org.argouml.uml.diagram.use_case.ui; import java.awt.Color; import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.MouseEvent; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.Vector; import javax.swing.Action; import org.argouml.model.Model; import org.argouml.ui.ArgoJMenu; import org.argouml.ui.targetmanager.TargetManager; import org.argouml.uml.diagram.DiagramSettings; import org.argouml.uml.diagram.ui.ActionAddExtensionPoint; import org.argouml.uml.diagram.ui.ActionAddNote; import org.argouml.uml.diagram.ui.ActionCompartmentDisplay; import org.argouml.uml.diagram.ui.FigCompartment; import org.argouml.uml.diagram.ui.FigCompartmentBox; import org.argouml.uml.diagram.ui.FigExtensionPointsCompartment; import org.tigris.gef.base.Selection; import org.tigris.gef.presentation.Fig; import org.tigris.gef.presentation.FigCircle; /** * A fig to display use cases on use case diagrams.<p> * * Realized as a solid oval containing the name of the use * case. Optionally may be split into two compartments, with the lower * compartment displaying the extension points for the use case.<p> * * Implements all interfaces through its superclasses.<p> * * There is some coordinate geometry to be done to fit rectangular * text boxes inside an ellipse, and to draw a horizontal line * at any height within the ellipse, touching the ellipse. * In the following, we start from a coordinate * system with the center at the center of the ellipse. * The rectangular text box contains the * name and any extension points if shown, and is deemed to be of * height <em>2h</em> and width <em>2w</em>. We allow a margin of * <em>p</em> above the top and below the bottom of the box, so we * know the height of the ellipse, <em>2b</em> = <em>2h</em> + * <em>2p</em>.<p> * * The formula for an ellipse of width <em>2a</em> and height * <em>2b</em>, centered on the origin, is<p> * * <em>x</em>^2/<em>a</em>^2 + <em>y</em>^2/<em>b</em>^2 = 1.<p> * or:<p> * x²/a² + y²/b² = 1<p> * * We know that a corner of the rectangle is at coordinate * (<em>w</em>,<em>h</em>), since the rectangle must also be centered * on the origin to fit within the ellipse. Substituting these values * for <em>x</em> and <em>y</em> in the formula above, we can compute * <em>a</em>, half the width of the ellipse, since we know * <em>b</em>.<p> * * <em>a</em> = <em>wb</em>/sqrt(<em>b</em>^2 - <em>h</em>^2).<p> * * But <em>b</em> was defined in terms of the height of the rectangle * plus agreed padding at the top, so we can write.<p> * * <em>a</em> = (<em>wh</em> + <em>wb</em>)/ * sqrt(2<em>hp</em> + <em>p</em>^2)<p> * * Given we now know <em>a</em> and <em>b</em>, we can find the * coordinates of any partition line required between use case name * and extension points.<p> * * Finally we need to transform our coordinates, to recognise that the * origin is at our top left corner, and the Y coordinates are * reversed.<p> */ public class FigUseCase extends FigCompartmentBox { /** * The minimum padding allowed above the rectangle for * the use case name and extension points to the top of the use * case oval itself. */ private static final int MIN_VERT_PADDING = 4; /** * The Fig for the extensionPoints compartment (if any). */ private FigExtensionPointsCompartment extensionPointsFigCompartment; /** * Initialization which is common to multiple constructors.<p> * * There should be no size calculations here, nor color setting, * since not all attributes are set yet (like e.g. fill color). */ private void initialize(Rectangle bounds) { enableSizeChecking(false); setSuppressCalcBounds(true); FigExtensionPointsCompartment epc = /* Side effect: This creates the fig: */ getExtensionPointsCompartment(); /* * A use case has an external separator. * External means external to the compartment box. * This horizontal line sticks out of the box, * and touches the ellipse edge. */ Fig separatorFig = epc.getSeparatorFig(); /* TODO: This next line prevent loading a UseCase * with a stereotype to grow. Why? */ getStereotypeFig().setVisible(true); // add Figs to the FigNode in back-to-front order addFig(getBigPort()); addFig(getNameFig()); // stereotype fig covers the name fig: addFig(getStereotypeFig()); addFig(epc); addFig(separatorFig); // Make all the parts match the main fig setFilled(true); super.setFillColor(FILL_COLOR); super.setLineColor(LINE_COLOR); super.setLineWidth(LINE_WIDTH); // by default, do not show extension points: setExtensionPointsVisible(false); /* Set the drop location in the case of D&D: */ if (bounds != null) { setLocation(bounds.x, bounds.y); } setSuppressCalcBounds(false); setBounds(getBounds()); enableSizeChecking(true); } @Override protected Fig createBigPortFig() { /* Use arbitrary dimensions for now. */ Fig b = new FigMyCircle(0, 0, 100, 60); b.setFilled(true); b.setFillColor(FILL_COLOR); b.setLineColor(LINE_COLOR); b.setLineWidth(LINE_WIDTH); return b; } /** * Construct a use case figure with the given owner, bounds, and rendering * settings. This constructor is used by the PGML parser. * * @param owner owning model element * @param bounds position and size * @param settings rendering settings */ public FigUseCase(Object owner, Rectangle bounds, DiagramSettings settings) { super(owner, bounds, settings); initialize(bounds); } /** * Build a collection of menu items relevant for a right-click * popup menu on a Use Case.<p> * * Adds to the generic pop up items from the parent.<p> * * @param me The mouse event that generated this popup. * * @return A collection of menu items */ @Override public Vector getPopUpActions(MouseEvent me) { /* Check if multiple items are selected: */ boolean ms = TargetManager.getInstance().getTargets().size() > 1; // Get the parent vector first Vector popUpActions = super.getPopUpActions(me); // Add menu to add an extension point or note. Placed one before last, // so the "Properties" entry is always last. ArgoJMenu addMenu = new ArgoJMenu("menu.popup.add"); if (!ms) { addMenu.add(ActionAddExtensionPoint.singleton()); } addMenu.add(new ActionAddNote()); popUpActions.add(popUpActions.size() - getPopupAddOffset(), addMenu); // Modifier menu. Placed one before last, so the "Properties" entry is // always last. popUpActions.add(popUpActions.size() - getPopupAddOffset(), buildModifierPopUp(LEAF | ROOT)); return popUpActions; } /** * Show menu to display/hide the extension point compartment. * @return the menu * @see org.argouml.uml.diagram.ui.FigNodeModelElement#buildShowPopUp() */ @Override protected ArgoJMenu buildShowPopUp() { ArgoJMenu showMenu = super.buildShowPopUp(); Iterator i = ActionCompartmentDisplay.getActions().iterator(); while (i.hasNext()) { showMenu.add((Action) i.next()); } return showMenu; } /** * USED BY PGML.tee. * @return the class name and bounds together with compartment * visibility. */ @Override public String classNameAndBounds() { return super.classNameAndBounds() + "extensionPointVisible=" + isExtensionPointsVisible(); } public boolean isExtensionPointsVisible() { return extensionPointsFigCompartment != null && extensionPointsFigCompartment.isVisible(); } /** * Set the visibility of the extension point compartment. This is * called from outside this class when the user sets visibility * explicitly through the style panel or the context sensitive * pop-up menu.<p> * * We don't change the size of the use case, so we just have to * mark the extension point elements' visibility. * {@link #setBounds(int, int, int, int)} will do the relayout * (with name in the middle) for us.<p> * * @param isVisible <code>true</code> if the compartment should be shown, * <code>false</code> otherwise. */ public void setExtensionPointsVisible(boolean isVisible) { setCompartmentVisible(extensionPointsFigCompartment, isVisible); } /** * Creates a set of handles for dragging generalization/specializations * or associations.<p> * * @return The new selection object (a GEF entity). */ @Override public Selection makeSelection() { return new SelectionUseCase(this); } /** * Compute the dimensions of an ellipse that intersects the 4 corners * of the given box. * * @param box the width and height of the box * @return the dimension of the ellipse */ public Dimension addCompartmentBoxSurroundings(Dimension box) { containerBox = box; @SuppressWarnings("unused") double h = box.height; double w = box.width; int padding = Math.max((int) (w / 10.0), MIN_VERT_PADDING); return calcEllipse(box, padding); } /** * A private utility to calculate the bounding oval for the given * rectangular text box.<p> * * To sufficiently constrain the problem, we define that there is a gap * given by the parameter <code>vertPadding</code> above the top of the * box to the top of the oval.<p> * * All computations are done in double, and then converted to integer at * the end.<p> * * @param rectSize The dimensions of the rectangle to be bounded * * @param vertPadding The padding between the top of the box and the top * of the ellipse. * * @return The dimensions of the required oval. */ private Dimension calcEllipse(Dimension rectSize, int vertPadding) { // Work out the radii of the ellipse, a and b. The top right corner of // the ellipse (Cartesian coordinates, centered on the origin) will be // at (x,y) double a; double b = rectSize.height / 2.0 + vertPadding; double x = rectSize.width / 2.0; double y = rectSize.height / 2.0; // Formula for a is described in the overall class description. a = (x * b) / Math.sqrt(b * b - y * y); // Result as integers, rounded up. We ensure that the radii are // integers for convenience. return new Dimension(((int) (Math.ceil(a) + getLineWidth()) * 2), ((int) (Math.ceil(b) + getLineWidth()) * 2)); } @Override protected Rectangle calculateCompartmentBoxDimensions( int x, int y, int w, int h) { /* For an ellipse, we can put the box in the middle: */ return new Rectangle( x + (w - containerBox.width) / 2, y + (h - containerBox.height) / 2, containerBox.width, containerBox.height); } @Override protected void setCompartmentBounds(FigCompartment c, Rectangle cb, Rectangle ob) { Rectangle r = new Rectangle(); r.y = cb.y; r.height = getLineWidth(); r.width = (int) (2.0 * (calcX( ob.width / 2.0, ob.height / 2.0, ob.height / 2.0 - (cb.y - ob.y)))); r.x = cb.x + cb.width / 2 - r.width / 2; c.setExternalSeparatorFigBounds(r); c.setBounds(cb.x, cb.y, cb.width, cb.height); } /** * Private utility routine to work out the (positive) x coordinate of a * point on an oval, given the radii and y coordinate.<p> * TODO: Use this to calculate the separator lines! * * @param a radius in X direction * @param b radius in Y direction * @param y Y coordinate * @return Positive X coordinate for the given Y coordinate */ private double calcX(double a, double b, double y) { assert a > 0; assert b > 0; assert b > y; return (a * Math.sqrt(b * b - y * y)) / b; } /** * Set the fill colour for the use case oval.<p> * * This involves setting the fill color of all figs, but not the bigPort. * Calling the super method would cause all FigGroup elements * to follow suit - which is not wanted for the bigPort nor the separator. * * @param col The colour desired. */ @Override public void setFillColor(Color col) { getBigPort().setFillColor(col); } public Color getFillColor() { return getBigPort().getFillColor(); } public boolean getFilled() { return getBigPort().isFilled(); } public boolean isFilled() { return getBigPort().isFilled(); } /** * Set whether the use case oval is to be filled.<p> * * This is overridden to have no effect as the use case is always filled * @param f this argument is ignored. */ @Override public void setFilled(boolean f) { // } /** * FigMyCircle is a FigCircle with corrected connectionPoint method: * this methods calculates where a connected edge ends.<p> * * TODO: Once we are at GEF version 0.13.1M4, this whole class can be * removed, since it was taken over by GEF. */ public static class FigMyCircle extends FigCircle { /** * Constructor just invokes the parent constructor.<p> * * @param x X coordinate of the upper left corner of the bounding * box. * * @param y Y coordinate of the upper left corner of the bounding * box. * * @param w Width of the bounding box. * * @param h Height of the bounding box. * * @param lColor Line colour of the fig. * * @param fColor Fill colour of the fig. */ public FigMyCircle(int x, int y, int w, int h, Color lColor, Color fColor) { super(x, y, w, h, lColor, fColor); } /** * Constructor just invokes the parent constructor.<p> * * @param x X coordinate of the upper left corner of the bounding * box. * * @param y Y coordinate of the upper left corner of the bounding * box. * * @param w Width of the bounding box. * * @param h Height of the bounding box. */ public FigMyCircle(int x, int y, int w, int h) { super(x, y, w, h); } /** * Compute the border point of the ellipse that is on the edge * between the stored upper left corner and the given parameter.<p> * * TODO: Once we are at GEF version 0.13.1M4, this method * and in fact the whole class can be * removed, since it was taken over by GEF in revision 1279. * * @param anotherPt The remote point to which an edge is drawn. * * @return The connection point on the boundary of the * ellipse. */ @Override public Point connectionPoint(Point anotherPt) { double rx = _w / 2; double ry = _h / 2; double dx = anotherPt.x - (_x + rx); double dy = anotherPt.y - (_y + ry); double dd = ry * ry * dx * dx + rx * rx * dy * dy; double mu = rx * ry / Math.sqrt(dd); Point res = new Point((int) (mu * dx + _x + rx), (int) (mu * dy + _y + ry)); return res; } } /* * Use the code from the FigCircle, not the one from Fig. */ @Override public Point connectionPoint(Point anotherPt) { return getBigPort().connectionPoint(anotherPt); } @Override protected void updateListeners(Object oldOwner, Object newOwner) { Set<Object[]> listeners = new HashSet<Object[]>(); /* Let's register for events from all modelelements * that change the name or body text: */ if (newOwner != null) { /* Register for name changes, added extensionPoints * and abstract makes the text italic. * All Figs need to listen to "remove", too: */ listeners.add(new Object[] {newOwner, new String[] {"remove", "name", "isAbstract", "extensionPoint", "stereotype"}}); // register for extension points: for (Object ep : Model.getFacade().getExtensionPoints(newOwner)) { listeners.add(new Object[] {ep, new String[] {"location", "name"}}); } for (Object st : Model.getFacade().getStereotypes(newOwner)) { listeners.add(new Object[] {st, "name"}); } } updateElementListeners(listeners); } @Override public void renderingChanged() { super.renderingChanged(); if (getOwner() != null) { updateExtensionPoints(); } } protected void updateExtensionPoints() { if (!isExtensionPointsVisible()) { return; } extensionPointsFigCompartment.populate(); setBounds(getBounds()); damage(); } /** * @return the Fig for the extension point compartment */ public FigExtensionPointsCompartment getExtensionPointsCompartment() { // Set bounds will be called from our superclass constructor before // our constructor has run, so make sure this gets set up if needed. if (extensionPointsFigCompartment == null) { extensionPointsFigCompartment = new FigExtensionPointsCompartment( getOwner(), DEFAULT_COMPARTMENT_BOUNDS, getSettings()); } return extensionPointsFigCompartment; } }