// $Id: FigUseCase.java,v 1.46 2004/10/03 17:50:18 mvw Exp $
// Copyright (c) 1996-2004 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.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.beans.PropertyVetoException;
import java.util.Collection;
import java.util.Iterator;
import java.util.Vector;
import org.apache.log4j.Logger;
import org.argouml.application.api.Notation;
import org.argouml.model.ModelFacade;
import org.argouml.model.uml.UmlHelper;
import org.argouml.ui.ArgoJMenu;
import org.argouml.ui.targetmanager.TargetManager;
import org.argouml.uml.diagram.ui.CompartmentFigText;
import org.argouml.uml.diagram.ui.FigNodeModelElement;
import org.argouml.uml.diagram.ui.ActionAddNote;
import org.argouml.uml.diagram.ui.ActionAddExtensionPoint;
import org.argouml.uml.generator.ParserDisplay;
import org.argouml.uml.diagram.ui.ActionCompartmentDisplay;
import org.argouml.uml.diagram.ui.ActionModifier;
import org.tigris.gef.base.Editor;
import org.tigris.gef.base.Globals;
import org.tigris.gef.base.Selection;
import org.tigris.gef.graph.GraphModel;
import org.tigris.gef.presentation.Fig;
import org.tigris.gef.presentation.FigCircle;
import org.tigris.gef.presentation.FigGroup;
import org.tigris.gef.presentation.FigLine;
import org.tigris.gef.presentation.FigRect;
import org.tigris.gef.presentation.FigText;
import ru.novosoft.uml.MElementEvent;
/**
* A fig to display use cases on use case diagrams.
*
* Realised 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.
*
* Implements all interfaces through its superclasses.
*
* There is some coordinate geometry to be done to fit rectangular
* text boxes inside an elipse. The rectangular text box contains the
* name and any extension points if shown, and is deemed to be of
* height 2h and width 2w. We allow a margin of
* p above the top and below the bottom of the box, so we
* know the height of the elipse, 2b = 2h +
* 2p.
*
* The formula for an elipse of width 2a and height
* 2b, centred on the origin, is
*
* x^2/a^2 + y^2/b^2 = 1.
*
* We know that a corner of the rectangle is at coordinate
* (w,h), since the rectangle must also be centred
* on the origin to fit within the elipse. Subsituting these values
* for x and y in the formula above, we can compute
* a, half the width of the elipse, since we know
* b.
*
* a = wb/sqrt(b^2 - h^2).
*
* But b was defined in terms of the height of the rectangle
* plus agreed padding at the top, so we can write.
*
* a = (wh + wb)/
* sqrt(2hp + p^2)
*
* Given we now know a and b, we can find the
* coordinates of any partition line required between use case name
* and extension points.
*
* Finally we need to transform our coordinates, to recognise that the
* origin is at our top left corner, and the Y coordinates are
* reversed.
*/
public class FigUseCase extends FigNodeModelElement {
private static final Logger LOG = Logger.getLogger(FigUseCase.class);
///////////////////////////////////////////////////////////////////////////
//
// Constants
//
///////////////////////////////////////////////////////////////////////////
/**
* The minimum padding allowed above and below the rectangle for
* the use case name and extension points to the top of the use
* case oval itself.
*/
protected static final int MIN_VERT_PADDING = 4;
/**
* Space above and below the line separating name from extension
* points. The line takes a further 1 pixel.
*/
protected static final int SPACER = 2;
///////////////////////////////////////////////////////////////////////////
//
// Instance variables
//
///////////////////////////////////////////////////////////////////////////
/**
* UML use cases do not really have ports, so just define one big
* one so that users can drag edges to or from any point in the
* icon.
*/
private FigMyCircle bigPort;
/**
* We don't use _bigPort for the actual graphics of the oval. We
* define an identical oval that sits on top of it.
*/
private FigMyCircle cover;
/**
* The line separating name and extension points.
*/
private FigLine epSep;
/**
* The vector of graphics for extension points (if any). First one
* is the rectangle for the entire extension points box.
*/
private FigGroup epVec;
/**
* The rectangle for the entire extension point box.
*/
private FigRect epBigPort;
/**
* Text highlighted by mouse actions on the diagram. Assumed to
* belong to the extension point compartment.
*/
private CompartmentFigText highlightedFigText = null;
///////////////////////////////////////////////////////////////////////////
//
// Constructors
//
///////////////////////////////////////////////////////////////////////////
/**
* Constructor for a new use case fig. We work out the smallest
* oval that will fit round.
*
* At creation the extension point box is not showing (for
* consistency with existing implementations). We can show it
* later.
*/
public FigUseCase() {
// Create all the things we need, then use getMinimumSize to work out
// the dimensions of the oval.
// First the main port ellipse and the cover of identical size that
// will realize it. Use arbitrary dimensions for now.
bigPort = new FigMyCircle(0, 0, 100, 60, Color.black, Color.white);
cover = new FigMyCircle(0, 0, 100, 60, Color.black, Color.white);
// Mark the text, but not the box as filled, mark that the name may
// use multiline text (a bit odd - how do we enter a multi-line
// name?).
getNameFig().setTextFilled(false);
getNameFig().setFilled(false);
getNameFig().setLineWidth(0);
getNameFig().setMultiLine(false);
// The separator, again with arbitrary bounds for now.
epSep = new FigLine(0, 30, 100, 100, Color.black);
epSep.setVisible(false);
// The surrounding box for the extension points, again with arbitrary
// bounds for now (but made the same width as the name field, so the
// name field width will dominate size calculations, but there is a
// space to double click in for a new EP. It is not filled
// (although we have to specify a fill color at creation), nor has it
// a surrounding line. Its bounds, which allow for one line (which is
// empty) are the same as for the name box at this stage.
epBigPort =
new FigRect(0, 30, getNameFig().getBounds().width, 20,
Color.black, Color.white);
epBigPort.setFilled(false);
epBigPort.setLineWidth(0);
epBigPort.setVisible(false);
// The group for the extension points. The first entry in the vector
// is the overall surrounding box itself. The group is not filled, nor
// has any line. The first entry we add is the epBigPort
epVec = new FigGroup();
epVec.setFilled(false);
epVec.setLineWidth(0);
epVec.setVisible(false);
epVec.addFig(epBigPort);
// We now use getMiniumSize to work out the dimensions of the ellipse
// that we need, and then reset the bounds of everything.
Dimension ellipse = getMinimumSize();
// The size of the port and cover ellipses
bigPort.setBounds(0, 0, ellipse.width, ellipse.height);
cover.setBounds(0, 0, ellipse.width, ellipse.height);
// Space for the name. Centred horizontally, and (since we are minimum
// size) _MIN_VERT_PADDING from the top.
Dimension nameSize = getNameFig().getMinimumSize();
getNameFig().setBounds((ellipse.width - nameSize.width) / 2,
MIN_VERT_PADDING,
nameSize.width,
nameSize.height);
getStereotypeFig().setBounds(0, 0, 0, 0);
// The separator. We cheat here. Since the name and extension points
// rectangles are the same size at this stage, this must be at the
// midpoint of the elipse.
epSep.setShape(0,
ellipse.height / 2,
ellipse.width,
ellipse.height / 2);
// The surrounding box for the extension points. At this stage we know
// the separator is 1 pixel wide at the midpoint, and there is _SPACER
// below this before the extension box.
Dimension epSize = epBigPort.getMinimumSize();
epBigPort.setBounds((ellipse.width - epSize.width) / 2,
ellipse.height / 2 + 1 + SPACER,
epSize.width,
epSize.height);
setBigPort(bigPort);
// add Figs to the FigNode in back-to-front order
addFig(bigPort);
addFig(cover);
addFig(getNameFig());
addFig(getStereotypeFig());
addFig(epSep);
addFig(epVec);
// Having built the figure, getBounds finds the enclosing rectangle,
// which we set as our bounds.
Rectangle r = getBounds();
setBounds(r.x, r.y, r.width, r.height);
}
/**
* A version of the constructor used to associated the Fig with a
* particular NSUML object.
*
* Used at creation time of a UseCase.
* And also when Add to Diagram is activated.
* However, the load routines use the main constructor and
* call setOwner directly.
*
* @param gm The graph model to associate with this Fig. Ignored in this
* implementation.
*
* @param node The NSUML object to associate with this Fig.
*/
public FigUseCase(GraphModel gm, Object node) {
this();
setOwner(node);
}
/**
* The text string to be used as the default name of the new use
* case fig. However this seems in general to be immediately
* overwritten - presumably somewhere in the creation code for the
* object, which choses to define a name.
*
* Note. Good UML would probably prefer a name starting
* with a capital and no spaces!
*
* @return The desired text of the default name.
*/
public String placeString() {
return "new Use Case";
}
/**
* Make a copy of the current fig.
*
* Uses the generic superclass clone which gives a vector of all
* the figs. Then initialize our instance variables from this
* vector.
*
* @return A new copy of the the current fig.
*/
public Object clone() {
FigUseCase figClone = (FigUseCase) super.clone();
Iterator it = figClone.getFigs(null).iterator();
figClone.bigPort = (FigMyCircle) it.next();
figClone.cover = (FigMyCircle) it.next();
figClone.setNameFig((FigText) it.next());
figClone.setStereotypeFig((FigText) it.next());
figClone.epSep = (FigLine) it.next();
figClone.epVec = (FigGroup) it.next();
return figClone;
}
///////////////////////////////////////////////////////////////////////////
//
// Fig accessors
//
///////////////////////////////////////////////////////////////////////////
/**
* Build a collection of menu items relevant for a right-click
* popup menu on a Use Case.
*
* Adds to the generic pop up items from the parent.
*
* @param me The mouse event that generated this popup.
*
* @return A collection of menu items
*/
public Vector getPopUpActions(MouseEvent me) {
// 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(BUNDLE, "menu.popup.add");
addMenu.add(ActionAddExtensionPoint.singleton());
addMenu.add(ActionAddNote.getSingleton());
popUpActions.insertElementAt(addMenu,
popUpActions.size() - POPUP_ADD_OFFSET);
// Show menu to display/hide the extension point compartment. Placed
// one before last, so the "Properties" entry is always last.
ArgoJMenu showMenu = new ArgoJMenu(BUNDLE, "menu.popup.show");
if (epVec.isVisible()) {
showMenu.add(ActionCompartmentDisplay.hideExtPointCompartment());
} else {
showMenu.add(ActionCompartmentDisplay.showExtPointCompartment());
}
popUpActions.insertElementAt(showMenu,
popUpActions.size() - POPUP_ADD_OFFSET);
// Modifier menu. Placed one before last, so the "Properties" entry is
// always last.
ArgoJMenu modifierMenu = new ArgoJMenu(BUNDLE, "menu.popup.modifiers");
Object useCase = /*(MUseCase)*/ getOwner();
modifierMenu.addCheckItem(new ActionModifier("Abstract",
"isAbstract",
"isAbstract",
"setAbstract",
useCase));
modifierMenu.addCheckItem(new ActionModifier("Leaf",
"isLeaf",
"isLeaf",
"setLeaf",
useCase));
modifierMenu.addCheckItem(new ActionModifier("Root",
"isRoot",
"isRoot",
"setRoot",
useCase));
popUpActions.insertElementAt(modifierMenu,
popUpActions.size() - POPUP_ADD_OFFSET);
return popUpActions;
}
/**
* Returns whether the extension points are currently displayed.
*
* @return true
if the attributes are visible,
* false
otherwise.
*/
public boolean isExtensionPointVisible() {
return epVec.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.
*
* 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.
*
* @param isVisible true
if the compartment should be shown,
* false
otherwise.
*/
public void setExtensionPointVisible(boolean isVisible) {
// Record our current bounds for later use
Rectangle oldBounds = getBounds();
// First case is where the extension points are currently displayed and
// we are asked to turn them off.
if (epVec.isVisible() & (!isVisible)) {
// Tell GEF that we are starting to make a change. Loop through the
// epVec marking each element as not visible.
Iterator it = epVec.getFigs(null).iterator();
while (it.hasNext()) {
((Fig) (it.next())).setVisible(false);
}
// Mark the vector itself and the separator as not displayed
epVec.setVisible(false);
epSep.setVisible(false);
// Redo the bounds and then tell GEF the change has finished
setBounds(oldBounds.x, oldBounds.y,
oldBounds.width,
oldBounds.height);
endTrans();
}
// Second case is where the extension points are not currently
// displayed and we are asked to turn them on.
else if ((!epVec.isVisible()) & isVisible) {
// Tell GEF that we are starting to make a change. Loop through the
// epVec marking each element as visible.
Iterator it = epVec.getFigs(null).iterator();
while (it.hasNext()) {
((Fig) (it.next())).setVisible(true);
}
// Mark the vector itself and the separator as displayed
epVec.setVisible(true);
epSep.setVisible(true);
// Redo the bounds and then tell GEF the change has finished
setBounds(oldBounds.x, oldBounds.y,
oldBounds.width,
oldBounds.height);
endTrans();
}
}
/**
* Creates a set of handles for dragging generalization/specializations
* or associations.
*
* @return The new selection object (a GEF entity).
*/
public Selection makeSelection() {
return new SelectionUseCase(this);
}
/**
* Compute the minimum acceptable size of the use case.
*
* We work out the minimum size of the text box, and from that the radii
* of the enclosing ellipse.
*
* @return The dimensions of the smallest size bounding box of the use
* case.
*/
public Dimension getMinimumSize() {
Dimension textSize = getTextSize();
Dimension size = calcEllipse(textSize, MIN_VERT_PADDING);
return new Dimension(Math.max(size.width, 100),
Math.max(size.height, 60));
}
/**
* A private utility routine to calculate the minimum size of the
* rectangle to hold the name and extension points (if displayed).
*
* @return The dimensions of the rectangle
*/
private Dimension getTextSize() {
Dimension minSize = getNameFig().getMinimumSize();
// Now allow for the extension points, if they are displayed
if (epVec.isVisible()) {
// Allow for a separator (spacer each side + 1 pixel width line)
minSize.height += 2 * SPACER + 1;
// Loop through all the extension points, to find the widest
// (remember the first fig is the box for the whole lot, so ignore
// it).
Iterator it = epVec.getFigs(null).iterator();
it.next(); // ignore
while (it.hasNext()) {
int elemWidth =
((FigText) it.next()).getMinimumSize().width;
minSize.width = Math.max(minSize.width, elemWidth);
}
// Height allows one row for each extension point (remember to
// ignore the first element, which is the box for the lot), subject
// to there always being space for at least one extension point.
minSize.height +=
ROWHEIGHT * Math.max(1, epVec.getFigs(null).size() - 1);
}
return minSize;
}
/**
* A private utility to calculate the bounding oval for the given
* rectangular text box.
*
* To sufficiently constrain the problem, we define that there is a gap
* given by the parameter vertPadding
above the top of the
* box to the top of the oval.
*
* All computations are done in double, and then converted to integer at
* the end.
*
* @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, centred 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)) * 2),
((int) (Math.ceil(b)) * 2));
}
/**
* Change the boundary of the use case.
*
* If we are called with less than the minimum size, we impose the
* minimum size.
*
* We place the name and extension points at the centre of the
* rectangle.
*
* Set the bounds of all components of the Fig.
*
* @param x X coordinate of upper left corner
*
* @param y Y coordinate of upper left corner
*
* @param w width of bounding box
*
* @param h height of bounding box
*/
public void setBounds(int x, int y, int w, int h) {
// Remember where we are at present, so we can tell GEF later. Then
// check we are as big as the minimum size
Rectangle oldBounds = getBounds();
Dimension minSize = getMinimumSize();
int newW = (minSize.width > w) ? minSize.width : w;
int newH = (minSize.height > h) ? minSize.height : h;
// Work out the size of the name and extension point rectangle, and
// hence the vertical padding
Dimension textSize = getTextSize();
int vPadding = (newH - textSize.height) / 2;
// Adjust the alignment of the name.
Dimension nameSize = getNameFig().getMinimumSize();
getNameFig().setBounds(x + ((newW - nameSize.width) / 2),
y + vPadding,
nameSize.width,
nameSize.height);
// Place extension points if they are showing
if (epVec.isVisible()) {
// currY tracks the current vertical position of each element. The
// separator is _SPACER pixels below the name. Its length is
// calculated from the formula for an ellipse.
int currY = y + vPadding + nameSize.height + SPACER;
int sepLen =
2 * (int) (calcX(newW / 2.0,
newH / 2.0,
newH / 2.0 - (currY - y)));
epSep.setShape(x + (newW - sepLen) / 2,
currY,
x + (newW + sepLen) / 2,
currY);
// Extension points are 1 pixel for the line and _SPACER gap below
// the separator
currY += 1 + SPACER;
// Use the utility routine getUpdatedSize to move the extension
// point figures. We can discard the result of this routine. For
// now we assume that extension points are the width of the overall
// text rectangle (true unless the name is wider than any EP).
updateFigGroupSize(epVec,
x + ((newW - textSize.width) / 2),
currY,
textSize.width,
(textSize.height - nameSize.height
- SPACER * 2 - 1));
}
// Set the bounds of the bigPort and cover
bigPort.setBounds(x, y, newW, newH);
cover.setBounds(x, y, newW, newH);
// Record the changes in the instance variables of our parent, tell GEF
// and trigger the edges to reconsider themselves.
_x = x;
_y = y;
_w = newW;
_h = newH;
firePropChange("bounds", oldBounds, getBounds());
updateEdges();
}
/**
* Private utility routine to work out the (positive) x coordinate of a
* point on an oval, given the radii and y coordinate.
*
* @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) {
return (a * Math.sqrt(b * b - y * y)) / b;
}
/**
* Set the line colour for the use case oval.
*
* This involves setting the _cover oval, not the bigPort.
*
* @param col The colour desired.
*/
public void setLineColor(Color col) {
cover.setLineColor(col);
}
/**
* Get the line colour for the use case oval.
*
* This involves getting the _cover oval colour, not the bigPort.
*
* @return The colour in use.
*/
public Color getLineColor() {
return cover.getLineColor();
}
/**
* Set the fill colour for the use case oval.
*
* This involves setting the _cover oval, not the bigPort.
*
* @param col The colour desired.
*/
public void setFillColor(Color col) {
cover.setFillColor(col);
}
/**
* Get the line colour for the use case oval.
*
* This involves getting the _cover oval colour, not the bigPort.
*
* @return The colour in use.
*/
public Color getFillColor() {
return cover.getFillColor();
}
/**
* Set whether the use case oval is to be filled.
*
* This involves setting the _cover oval, not the bigPort.
*
* @param f true
if the oval is to be filled,
* false
if not.
*/
public void setFilled(boolean f) {
cover.setFilled(f);
}
/**
* Get whether the use case oval is to be filled.
*
* This involves getting the _cover oval, not the bigPort.
*
* @return true
if the oval is to be filled,
* false
if not.
*/
public boolean getFilled() {
return cover.getFilled();
}
/**
* Set the line width for the use case oval.
*
* This involves setting the _cover oval, not the bigPort.
*
* @param w The line width desired.
*/
public void setLineWidth(int w) {
cover.setLineWidth(w);
}
/**
* Get the line width for the use case oval.
*
* This involves getting the _cover oval colour, not the bigPort.
*
* @return The line width set.
*/
public int getLineWidth() {
return cover.getLineWidth();
}
/**
* FigMyCircle is a FigCircle with corrected connectionPoint method:
* this methods calculates where a connected edge ends.
*/
public class FigMyCircle extends FigCircle {
/**
* Constructor just invokes the parent constructor.
*
* @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);
}
/**
* Compute the border point of the elipse that is on the edge
* between the stored upper left corner and the given parameter.
*
* @param anotherPt The remote point to which an edge is drawn.
*
* @return The connection point on the boundary of the
* elipse.
*/
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));
LOG.debug(" returns " + res.x + ',' + res.y + ')');
return res;
}
}
///////////////////////////////////////////////////////////////////////////
//
// Event handlers
//
///////////////////////////////////////////////////////////////////////////
/**
* React to a mouse key being pressed.
*
* @param me The mouse action that caused us to be invoked.
*/
public void mousePressed(MouseEvent me) {
// Deal with anything from the parent first
super.mousePressed(me);
// If we are currently selected, turn off the draggable buttons at each
// side, and unhighlight any currently selected extension points.
Editor ce = Globals.curEditor();
Selection sel = ce.getSelectionManager().findSelectionFor(this);
if (sel instanceof SelectionUseCase) {
((SelectionUseCase) sel).hideButtons();
}
unhighlight();
// Display extension point properties if necessary. Look to see if the
// mouse (2x2 pixels) hit the extension point compartment. Use a flag
// to track this.
Rectangle r = new Rectangle(me.getX() - 1, me.getY() - 1, 2, 2);
Fig f = hitFig(r);
boolean targetIsSet = false;
if (f == epVec) {
// Work out which extension point this corresponds to. Each EP
// takes ROWHEIGHT pixels, so take the difference between the
// centre of the mouse (me.getY() - 1) and the top of the epVec
// (f.getY()) and integer divide by ROWHEIGHT.
// TODO: in future version of GEF call getFigs returning array
Vector v = new Vector(epVec.getFigs(null));
int i = (me.getY() - f.getY() - 1) / ROWHEIGHT;
// If we are in the range of the EP list size (avoids any nasty
// boundary overflows), we can select that EP entry. Make this
// entry the target Fig, and note that we do have a
// target. Remember that the first entry in the vector is the
// bigPort itself.
if ((i >= 0) && (i < (v.size() - 1))) {
targetIsSet = true;
f = (Fig) v.elementAt(i + 1);
highlightedFigText = (CompartmentFigText) f;
highlightedFigText.setHighlighted(true);
}
}
// If we didn't get the EP compartment, we just select ourself.
if (!targetIsSet) {
TargetManager.getInstance().setTarget(getOwner());
}
}
/**
* React to a mouse key being clicked.
*
* @param me The mouse action that caused us to be invoked.
*/
public void mouseClicked(MouseEvent me) {
super.mouseClicked(me);
if (me.isConsumed())
return;
if (!isExtensionPointVisible() || me.getY() < epSep.getY1()) {
getNameFig().mouseClicked(me);
} else if (me.getClickCount() >= 2
&& !(me.isPopupTrigger()
|| me.getModifiers() == InputEvent.BUTTON3_MASK)) {
createFeatureIn(epVec, me);
}
}
/**
* Deal with the mouse leaving the fig. Unhighlight the fig.
*
* @param me The mouse action that caused us to be invoked.
*/
public void mouseExited(MouseEvent me) {
super.mouseExited(me);
unhighlight();
}
/**
* Deal with a key being pressed.
*
* We deal with UP and DOWN, and use these to move through the list
* of selected extension points. We deal with ENTER and use that to start
* the text editor.
*
* @param ke The key event that caused us to be invoked.
*/
public void keyPressed(KeyEvent ke) {
int key = ke.getKeyCode();
// For UP and DOWN cycle through the extension points.
if ((key == KeyEvent.VK_UP) || (key == KeyEvent.VK_DOWN)) {
// Find the currently highlighted EP (if any) and unhighlight it
CompartmentFigText ft = unhighlight();
if (ft != null) {
// TODO: in future version of GEF call getFigs returning array
int i = new Vector(epVec.getFigs(null)).indexOf(ft);
// If we found one of the current EP's move forward or
// backwards as appopriate, and set a newly selected EP as
// highlighted
if (i != -1) {
if (key == KeyEvent.VK_UP) {
ft = getPreviousVisibleFeature(epVec, i);
} else {
ft = getNextVisibleFeature(epVec, i);
}
if (ft != null) {
ft.setHighlighted(true);
highlightedFigText = ft;
return;
}
}
}
}
// For ENTER start editing any text that is highlighted, remembering to
// consume the ENTER event.
else if ((key == KeyEvent.VK_ENTER) && (highlightedFigText != null)) {
highlightedFigText.startTextEditor(ke);
ke.consume();
return;
}
// Anything else defer to our parent.
else {
super.keyPressed(ke);
}
}
///////////////////////////////////////////////////////////////////////////
//
// Internal methods
//
///////////////////////////////////////////////////////////////////////////
/**
* Invoked when text has been edited.
*
* We check that it is one of the extension point compartments and then
* parse accordingly.
*
* The parameter ft is the text that has been edited.
*
* @see org.argouml.uml.diagram.ui.FigNodeModelElement#textEdited(org.tigris.gef.presentation.FigText)
*/
protected void textEdited(FigText ft) throws PropertyVetoException {
// Let the parent do anything it wants first
super.textEdited(ft);
// Only works if we have an owner
Object useCase = /*(MUseCase)*/ getOwner();
if (useCase == null) {
return;
}
// Give up if we are not one of the extension points
// TODO: in future version of GEF call getFigs returning array
int i = new Vector(epVec.getFigs(null)).indexOf(ft);
if (i == -1) {
return;
}
// Mark the text as highlighted, then parse it
/* TODO: "highlightedFigText hides a field" Warning! Is intentional? */
CompartmentFigText highlightedFigText = (CompartmentFigText) ft;
highlightedFigText.setHighlighted(true);
Object ep = /*(MExtensionPoint)*/ (highlightedFigText.getOwner());
String text = highlightedFigText.getText().trim();
ParserDisplay.SINGLETON.parseExtensionPointFig(useCase, ep, text);
return;
}
/**
* Private method to find the previous visible feature to highlight.
*
* We're passed in a group (which will be the extension point vector)
* and the currently highlighted fig (a member of that vector).
*
* @param fgVec A fig group (invariably the extension point vector) in
* which to seek the previous visible feature.
*
* @param i The index of the currently selected entry in
* fgVec
.
*
* @return The new fig to use.
*/
private CompartmentFigText getPreviousVisibleFeature(FigGroup fgVec,
int i)
{
// Give up if the index we don't have a vector, or the index is less
// than 1 (the 0th entry is the bigPort surrounding all entries)
if ((fgVec == null) || (i < 1)) {
return null;
}
// Give up if we are off the top of the vector, or the indentified
// element is not displayed
// TODO: in future version of GEF call getFigs returning array
Vector v = new Vector(fgVec.getFigs(null));
if ((i >= v.size()) || (!((FigText) v.elementAt(i)).isVisible())) {
return null;
}
// Loop backwards through until we find an entry that is displayed. We
// know this will terminate, since the current element is displayed
CompartmentFigText cft = null;
do {
i = (i <= 1) ? i - 1 : v.size() - 1;
cft = (CompartmentFigText) v.elementAt(i);
} while (!cft.isVisible());
return cft;
}
/**
* Private method to find the next visible feature to highlight.
*
* We're passed in a group (which will be the extension point vector)
* and the currently highlighted fig (a member of that vector).
*
* @param fgVec A fig group (invariably the extension point vector) in
* which to seek the next visible feature.
*
* @param i The index of the currently selected entry in
* fgVec
.
*
* @return The new fig to use.
*/
private CompartmentFigText getNextVisibleFeature(FigGroup fgVec, int i) {
// Give up if the index we don't have a vector, or the index is less
// than 1 (the 0th entry is the bigPort surrounding all entries)
if ((fgVec == null) || (i < 1)) {
return null;
}
// Give up if we are off the top of the vector, or the indentified
// element is not displayed
Vector v = new Vector(fgVec.getFigs(null));
if ((i >= v.size()) || (!((FigText) v.elementAt(i)).isVisible())) {
return null;
}
// Loop forwards through until we find an entry that is displayed. We
// know this will terminate, since the current element is displayed
CompartmentFigText cft = null;
do {
i = (i >= (v.size() - 1)) ? 1 : i + 1;
cft = (CompartmentFigText) v.elementAt(i);
} while (!cft.isVisible());
return cft;
}
/**
* Create a new "feature" (extension point) in the use case fig.
*
* Extension points are not strictly features, but that is a historical
* accident of naming. This creates a new entry in the extension point
* vector.
*
* @param fg The fig group to which this applies (which must be the
* extension point vector).
*
* @param ie The input event that triggered us. In the current
* implementation a mouse double click.
*/
protected void createFeatureIn(FigGroup fg, InputEvent ie) {
// Give up if we don't have an owner
if (getOwner() == null) {
return;
}
// Invoke the relevant action method to create an empty extension
// point, then start the editor, assuming we successfully created an
// extension point.
ActionAddExtensionPoint.singleton().actionPerformed(null);
// TODO: in future version of GEF call getFigs returning array
CompartmentFigText ft =
(CompartmentFigText) new Vector(fg.getFigs(null)).lastElement();
if (ft != null) {
ft.startTextEditor(ie);
ft.setHighlighted(true);
highlightedFigText = ft;
}
ie.consume();
}
/**
* Private utility to unhighlight any currently selected extension
* point.
*
* @return The extension point that was unhighlighted.
*/
private CompartmentFigText unhighlight() {
// Loop through the vector of extension points, until we find a
// highlighted one.
Vector v = new Vector(epVec.getFigs(null));
int i;
for (i = 1; i < v.size(); i++) {
CompartmentFigText ft = (CompartmentFigText) v.elementAt(i);
if (ft.isHighlighted()) {
ft.setHighlighted(false);
highlightedFigText = null;
return ft;
}
}
// None were highlighted
return null;
}
/**
* Adjust the fig in the light of some change to the model.
*
* Called both when there has been a change to notation, and when there
* has been an NSUML event.
* @see org.argouml.uml.diagram.ui.FigNodeModelElement#modelChanged(ru.novosoft.uml.MElementEvent)
*/
protected void modelChanged(MElementEvent mee) {
// Let our superclass sort itself out first
super.modelChanged(mee);
if (mee == null
|| mee.getName().equals("extensionPoint")
|| org.argouml.model.ModelFacade.isAExtensionPoint(mee.getSource()))
{
updateExtensionPoint();
return;
}
if (mee == null || mee.getName().equals("isAbstract")) {
updateNameText();
}
}
/**
* Updates the extensionpoints in the fig
*/
protected void updateExtensionPoint() {
// Give up if we have no owner
Object useCase = /*(MUseCase)*/ getOwner();
if (useCase == null) {
return;
}
// Note our current bounds
Rectangle oldBounds = getBounds();
// Loop through all the extension points. epCount keeps track of the
// fig's index as we go through the extension points.
Collection eps =
UmlHelper.getHelper().getUseCases().getExtensionPoints(useCase);
int epCount = 1;
if (eps != null) {
int xpos = epBigPort.getX();
int ypos = epBigPort.getY();
// Take each EP and its corresponding fig in turn
Iterator iter = eps.iterator();
Vector figs = new Vector(epVec.getFigs(null));
while (iter.hasNext()) {
CompartmentFigText epFig;
Object ep = /*(MExtensionPoint)*/ iter.next();
// If we don't have a fig for this EP, we'll need to add
// one. We set the bounds, but they will be reset later.
if (figs.size() <= epCount) {
epFig =
new CompartmentFigText(xpos,
ypos + (epCount - 1) * ROWHEIGHT,
0,
ROWHEIGHT,
epBigPort);
epFig.setFilled(false);
epFig.setLineWidth(0);
epFig.setFont(getLabelFont());
epFig.setTextColor(Color.black);
epFig.setJustification(FigText.JUSTIFY_LEFT);
epFig.setMultiLine(false);
epVec.addFig(epFig);
} else {
epFig = (CompartmentFigText) figs.elementAt(epCount);
}
// Now put the text in
// We must handle the case where the text is null
String epText = Notation.generate(this, ep);
if (epText == null) {
epText = "";
}
epFig.setText(epText);
epFig.setOwner(ep);
epCount++;
}
// Remove any spare figs we have if there are now fewer extension
// points than figs
if (figs.size() > epCount) {
for (int i = figs.size() - 1; i >= epCount; i--) {
epVec.removeFig((Fig) figs.elementAt(i));
}
}
}
// Now recalculate all the bounds, using our old bounds.
setBounds(oldBounds.x, oldBounds.y, oldBounds.width, oldBounds.height);
}
/**
* @see org.argouml.uml.diagram.ui.FigNodeModelElement#updateNameText()
*/
protected void updateNameText() {
Object useCase = /*(MUseCase)*/ getOwner();
if (useCase == null)
return;
Rectangle oldBounds = getBounds();
super.updateNameText();
// Now things to do with the use case itself. Put the use case in
// italics if it is abstract, otherwise ordinary font.
if (ModelFacade.isAbstract(useCase)) {
getNameFig().setFont(getItalicLabelFont());
} else {
getNameFig().setFont(getLabelFont());
}
setBounds(oldBounds.x, oldBounds.y, oldBounds.width, oldBounds.height);
}
/**
* Makes sure that the edges stick to the elipse fig of the usecase.
* @see org.tigris.gef.presentation.Fig#getGravityPoints()
*/
public Vector getGravityPoints() {
Vector ret = new Vector();
int cx = bigPort.center().x;
int cy = bigPort.center().y;
int radiusx = Math.round(bigPort.getWidth() / 2) + 1;
int radiusy = Math.round(bigPort.getHeight() / 2) + 1;
final int maxPoints = 20;
Point point = null;
for (int i = 0; i < maxPoints; i++) {
point =
new Point((int) (cx
+ (Math.cos(2 * Math.PI / maxPoints * i)
* radiusx)),
(int) (cy
+ (Math.sin(2 * Math.PI / maxPoints * i)
* radiusy)));
ret.add(point);
}
return ret;
}
/**
* @see
* org.argouml.uml.diagram.ui.FigNodeModelElement#updateStereotypeText()
*/
protected void updateStereotypeText() {
super.updateStereotypeText();
if (!(getStereotype() == null || getStereotype().equals(""))) {
getStereotypeFig().setBounds((bigPort.getX()
+ bigPort.getWidth() / 2
- getStereotypeFig().getWidth() / 2),
(bigPort.getY()
+ bigPort.getHeight()
+ MIN_VERT_PADDING),
getStereotypeFig().getWidth(),
getStereotypeFig().getHeight());
} else {
getStereotypeFig().setBounds(0, 0, 0, 0);
}
damage();
}
/**
* @see org.tigris.gef.presentation.Fig#postLoad()
*/
public void postLoad() {
super.postLoad();
if (epVec != null && epVec.isVisible()) {
setExtensionPointVisible(true);
}
}
} /* end class FigUseCase */