/* $Id: FigComment.java 18728 2010-09-10 09:29:47Z mvw $
*****************************************************************************
* 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:
* Michiel van der Wulp
*****************************************************************************
*
* Some portions of this file was previously release using the BSD License:
*/
// 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.static_structure.ui;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.VetoableChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import javax.swing.SwingUtilities;
import org.apache.log4j.Logger;
import org.argouml.kernel.DelayedChangeNotify;
import org.argouml.kernel.DelayedVChangeListener;
import org.argouml.model.AttributeChangeEvent;
import org.argouml.model.Model;
import org.argouml.model.RemoveAssociationEvent;
import org.argouml.model.UmlChangeEvent;
import org.argouml.ui.ArgoJMenu;
import org.argouml.uml.diagram.DiagramSettings;
import org.argouml.uml.diagram.ui.FigMultiLineText;
import org.argouml.uml.diagram.ui.FigNodeModelElement;
import org.tigris.gef.base.Geometry;
import org.tigris.gef.base.Selection;
import org.tigris.gef.presentation.Fig;
import org.tigris.gef.presentation.FigPoly;
import org.tigris.gef.presentation.FigRect;
import org.tigris.gef.presentation.FigText;
/**
* Class to display a UML comment in a diagram.
*
* @author Andreas Rueckert
*/
public class FigComment
extends FigNodeModelElement
implements VetoableChangeListener,
DelayedVChangeListener,
MouseListener,
KeyListener,
PropertyChangeListener {
/**
* Logger.
*/
private static final Logger LOG = Logger.getLogger(FigComment.class);
////////////////////////////////////////////////////////////////
// constants
private int width = 80;
private int height = 60;
/**
* A dog-ear is a bent corner in a book.
*/
private int dogear = 10;
private boolean readyToEdit = true;
// The figure that holds the text of the note.
private FigText bodyTextFig;
private FigPoly outlineFig;
/**
* The upper right corner.
*/
private FigPoly urCorner;
/**
* Flag to indicate that we have just been created. This is to fix the
* problem with loading comments that have stereotypes already
* defined.<p>
*/
private boolean newlyCreated;
@Override
protected Fig createBigPortFig() {
FigRect fr = new FigRect(0, 0, width, height, null, null);
fr.setFilled(false);
fr.setLineWidth(0);
return fr;
}
private void initialize() {
Color fg = super.getLineColor(); // Use super because not fully init'd
Color fill = super.getFillColor();
outlineFig = new FigPoly(fg, fill);
outlineFig.addPoint(0, 0);
outlineFig.addPoint(width - 1 - dogear, 0);
outlineFig.addPoint(width - 1, dogear);
outlineFig.addPoint(width - 1, height - 1);
outlineFig.addPoint(0, height - 1);
outlineFig.addPoint(0, 0);
outlineFig.setFilled(true);
outlineFig.setLineWidth(LINE_WIDTH);
urCorner = new FigPoly(fg, fill);
urCorner.addPoint(width - 1 - dogear, 0);
urCorner.addPoint(width - 1, dogear);
urCorner.addPoint(width - 1 - dogear, dogear);
urCorner.addPoint(width - 1 - dogear, 0);
urCorner.setFilled(true);
Color col = outlineFig.getFillColor();
urCorner.setFillColor(col.darker());
urCorner.setLineWidth(LINE_WIDTH);
// add Figs to the FigNode in back-to-front order
addFig(getBigPort());
addFig(outlineFig);
addFig(urCorner);
addFig(getStereotypeFig());
addFig(bodyTextFig);
col = outlineFig.getFillColor();
urCorner.setFillColor(col.darker());
setBlinkPorts(false); //make port invisible unless mouse enters
Rectangle r = getBounds();
setBounds(r.x, r.y, r.width, r.height);
updateEdges();
readyToEdit = false;
// Mark this as newly created. This is to get round the problem with
// creating figs for loaded comments that had stereotypes. They are
// saved with their dimensions INCLUDING the stereotype, but since we
// pretend the stereotype is not visible, we add height the first time
// we render such a comment. This is a complete fudge, and really we
// ought to address how comment objects with stereotypes are saved. But
// that will be hard work.
newlyCreated = true;
}
/**
* Construct a comment figure with the given model element, bounds, and
* settings. This constructor is used by the PGML parser.
*
* @param owner owning Comments
* @param bounds position and size
* @param settings rendering settings
*/
public FigComment(Object owner, Rectangle bounds,
DiagramSettings settings) {
super(owner, bounds, settings);
bodyTextFig = new FigMultiLineText(getOwner(),
new Rectangle(2, 2, width - 2 - dogear, height - 4),
getSettings(), true);
initialize();
updateBody();
if (bounds != null) {
setLocation(bounds.x, bounds.y);
}
}
/**
* Get the default text for this figure.
*
* @return The default text for this figure.
*/
@Override
public String placeString() {
String placeString = retrieveBody();
if (placeString == null) {
// TODO: I18N
placeString = "new note";
}
return placeString;
}
/**
* Clone this figure.
*
* @return The cloned figure.
*/
@Override
public Object clone() {
FigComment figClone = (FigComment) super.clone();
Iterator thisIter = this.getFigs().iterator();
while (thisIter.hasNext()) {
Object thisFig = thisIter.next();
if (thisFig == outlineFig) {
figClone.outlineFig = (FigPoly) thisFig;
}
if (thisFig == urCorner) {
figClone.urCorner = (FigPoly) thisFig;
}
if (thisFig == bodyTextFig) {
figClone.bodyTextFig = (FigText) thisFig;
}
}
return figClone;
}
private void updateBody() {
if (getOwner() != null) {
String body = (String) Model.getFacade().getBody(getOwner());
if (body != null) {
bodyTextFig.setText(body);
}
}
}
/**
* See FigNodeModelElement.java for more info on these methods.
*/
/**
* If the user double clicks on any part of this FigNode, pass it
* down to one of the internal Figs. This allows the user to
* initiate direct text editing.
*
* {@inheritDoc}
*/
@Override
public void mouseClicked(MouseEvent me) {
if (!readyToEdit) {
Object owner = getOwner();
if (Model.getFacade().isAModelElement(owner)
&& !Model.getModelManagementHelper().isReadOnly(owner)) {
readyToEdit = true;
} else {
LOG.debug("not ready to edit note");
return;
}
}
if (me.isConsumed()) {
return;
}
if (me.getClickCount() >= 2
&& !(me.isPopupTrigger()
|| me.getModifiers() == InputEvent.BUTTON3_MASK)) {
if (getOwner() == null) {
return;
}
Fig f = hitFig(new Rectangle(me.getX() - 2, me.getY() - 2, 4, 4));
if (f instanceof MouseListener) {
((MouseListener) f).mouseClicked(me);
}
}
me.consume();
}
/*
* @see java.beans.VetoableChangeListener#vetoableChange(java.beans.PropertyChangeEvent)
*/
@Override
public void vetoableChange(PropertyChangeEvent pce) {
Object src = pce.getSource();
if (src == getOwner()) {
DelayedChangeNotify delayedNotify =
new DelayedChangeNotify(this, pce);
SwingUtilities.invokeLater(delayedNotify);
} else {
LOG.debug("FigNodeModelElement got vetoableChange"
+ " from non-owner:" + src);
}
}
/*
* @see org.argouml.kernel.DelayedVChangeListener#delayedVetoableChange(java.beans.PropertyChangeEvent)
*/
@Override
public void delayedVetoableChange(PropertyChangeEvent pce) {
// update any text, colors, fonts, etc.
renderingChanged();
// update the relative sizes and positions of internel Figs
endTrans();
}
/*
* @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
*/
@Override
public void propertyChange(PropertyChangeEvent pve) {
Object src = pve.getSource();
String pName = pve.getPropertyName();
if (pName.equals("editing")
&& Boolean.FALSE.equals(pve.getNewValue())) {
//parse the text that was edited
textEdited((FigText) src);
// resize the FigNode to accomodate the new text
Rectangle bbox = getBounds();
Dimension minSize = getMinimumSize();
bbox.width = Math.max(bbox.width, minSize.width);
bbox.height = Math.max(bbox.height, minSize.height);
setBounds(bbox.x, bbox.y, bbox.width, bbox.height);
endTrans();
} else {
super.propertyChange(pve);
}
}
/*
* @see java.awt.event.KeyListener#keyPressed(java.awt.event.KeyEvent)
*/
@Override
public void keyPressed(KeyEvent ke) {
// Not used, do nothing.
}
/*
* @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent)
*/
@Override
public void keyReleased(KeyEvent ke) {
// Not used, do nothing.
}
/*
* @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent)
*/
@Override
public void keyTyped(KeyEvent ke) {
if (Character.isISOControl(ke.getKeyChar())) {
return;
}
if (!readyToEdit) {
Object owner = getOwner();
if (Model.getFacade().isAModelElement(owner)
&& !Model.getModelManagementHelper().isReadOnly(owner)) {
storeBody("");
readyToEdit = true;
} else {
LOG.debug("not ready to edit note");
return;
}
}
if (ke.isConsumed()) {
return;
}
if (getOwner() == null) {
return;
}
bodyTextFig.keyTyped(ke);
}
////////////////////////////////////////////////////////////////
// Fig accessors
/**
* @return an empty menu
* @see org.argouml.uml.diagram.ui.FigNodeModelElement#buildShowPopUp()
*/
@Override
protected ArgoJMenu buildShowPopUp() {
return new ArgoJMenu("menu.popup.show");
}
/*
* @see org.tigris.gef.presentation.Fig#makeSelection()
*/
@Override
public Selection makeSelection() {
return new SelectionComment(this);
}
/*
* @see org.tigris.gef.presentation.Fig#setLineColor(java.awt.Color)
*/
@Override
public void setLineColor(Color col) {
// The text element has no border, so the line color doesn't matter.
outlineFig.setLineColor(col);
urCorner.setLineColor(col);
}
/*
* @see org.tigris.gef.presentation.Fig#getLineColor()
*/
@Override
public Color getLineColor() {
return outlineFig.getLineColor();
}
/*
* @see org.tigris.gef.presentation.Fig#setFillColor(java.awt.Color)
*/
@Override
public void setFillColor(Color col) {
outlineFig.setFillColor(col);
urCorner.setFillColor(col);
}
/*
* @see org.tigris.gef.presentation.Fig#getFillColor()
*/
@Override
public Color getFillColor() {
return outlineFig.getFillColor();
}
/*
* @see org.tigris.gef.presentation.Fig#setFilled(boolean)
*/
@Override
public void setFilled(boolean f) {
bodyTextFig.setFilled(false); // The text is always opaque.
outlineFig.setFilled(f);
urCorner.setFilled(f);
}
@Override
public boolean isFilled() {
return outlineFig.isFilled();
}
/*
* @see org.tigris.gef.presentation.Fig#setLineWidth(int)
*/
@Override
public void setLineWidth(int w) {
bodyTextFig.setLineWidth(0); // Make a seamless integration of the text
// in the note figure.
outlineFig.setLineWidth(w);
urCorner.setLineWidth(w);
}
/*
* @see org.tigris.gef.presentation.Fig#getLineWidth()
*/
@Override
public int getLineWidth() {
return outlineFig.getLineWidth();
}
////////////////////////////////////////////////////////////////
// user interaction methods
/*
* @see org.argouml.uml.diagram.ui.FigNodeModelElement#textEdited(org.tigris.gef.presentation.FigText)
*/
@Override
protected void textEdited(FigText ft) {
if (ft == bodyTextFig) {
storeBody(ft.getText());
}
}
/*
* @see org.argouml.uml.diagram.ui.FigNodeModelElement#textEditStarted(org.tigris.gef.presentation.FigText)
*/
@Override
protected void textEditStarted(FigText ft) {
showHelp("parsing.help.comment");
}
/*
* @see org.tigris.gef.presentation.Fig#setEnclosingFig(org.tigris.gef.presentation.Fig)
*/
@Override
public void setEnclosingFig(Fig encloser) {
super.setEnclosingFig(encloser);
}
////////////////////////////////////////////////////////////////
// accessor methods
/**
* Stores the body text in the associated model element.
*
* @param body The body text to store.
*/
public final void storeBody(String body) {
if (getOwner() != null) {
Model.getCoreHelper().setBody(getOwner(), body);
}
}
/**
* Retrieve the body text from the associated model element.
*
* @return The body from the associated model element.
*/
private String retrieveBody() {
return (getOwner() != null)
? (String) Model.getFacade().getBody(getOwner())
: null;
}
/*
* @see org.tigris.gef.presentation.Fig#getUseTrapRect()
*/
@Override
public boolean getUseTrapRect() {
return true;
}
/**
* Always returns null as the FigComment does not display its name.
* @return null
*/
@Override
public Rectangle getNameBounds() {
return null;
}
/**
* Get the minimum size for the note figure.
*
* @return The minimum size for the note figure.
*/
@Override
public Dimension getMinimumSize() {
// Get the size of the text field.
Dimension aSize = bodyTextFig.getMinimumSize();
// If we have a stereotype displayed, then allow some space for that
// (width and height)
if (getStereotypeFig().isVisible()) {
Dimension stereoMin = getStereotypeFig().getMinimumSize();
aSize.width =
Math.max(aSize.width,
stereoMin.width);
aSize.height += stereoMin.height;
}
// And add the gaps around the textfield to get the minimum
// size of the note.
return new Dimension(aSize.width + 4 + dogear,
aSize.height + 4);
}
/*
* @see org.tigris.gef.presentation.Fig#setBounds(int, int, int, int)
*/
@Override
protected void setStandardBounds(int px, int py, int w, int h) {
if (bodyTextFig == null) {
return;
}
Dimension stereoMin = getStereotypeFig().getMinimumSize();
int stereotypeHeight = 0;
if (getStereotypeFig().isVisible()) {
stereotypeHeight = stereoMin.height;
}
Rectangle oldBounds = getBounds();
// Resize the text figure
bodyTextFig.setBounds(px + 2, py + 2 + stereotypeHeight,
w - 4 - dogear, h - 4 - stereotypeHeight);
getStereotypeFig().setBounds(px + 2, py + 2,
w - 4 - dogear, stereoMin.height);
// Resize the big port around the figure
getBigPort().setBounds(px, py, w, h);
// Since this is a complex polygon, there's no easy way to resize it.
Polygon newPoly = new Polygon();
newPoly.addPoint(px, py);
newPoly.addPoint(px + w - 1 - dogear, py);
newPoly.addPoint(px + w - 1, py + dogear);
newPoly.addPoint(px + w - 1, py + h - 1);
newPoly.addPoint(px, py + h - 1);
newPoly.addPoint(px, py);
outlineFig.setPolygon(newPoly);
// Just move the corner to it's new position.
urCorner.setBounds(px + w - 1 - dogear, py, dogear, dogear);
calcBounds(); //_x = x; _y = y; _w = w; _h = h;
firePropChange("bounds", oldBounds, getBounds());
}
/*
* TODO: This is the same as in parent - remove?
* @see org.argouml.uml.diagram.ui.FigNodeModelElement#updateBounds()
*/
@Override
protected void updateBounds() {
Rectangle bbox = getBounds();
Dimension minSize = getMinimumSize();
bbox.width = Math.max(bbox.width, minSize.width);
bbox.height = Math.max(bbox.height, minSize.height);
setBounds(bbox.x, bbox.y, bbox.width, bbox.height);
}
///////////////////////////////////////////////////////////////////
// Internal methods
@Override
protected final void updateLayout(UmlChangeEvent mee) {
super.updateLayout(mee);
if (mee instanceof AttributeChangeEvent
&& mee.getPropertyName().equals("body")) {
bodyTextFig.setText(mee.getNewValue().toString());
calcBounds();
setBounds(getBounds());
damage();
} else if (mee instanceof RemoveAssociationEvent
&& mee.getPropertyName().equals("annotatedElement")) {
/* Remove the commentedge.
* If there are more then one comment-edges between
* the 2 objects, then delete them all. */
Collection<FigEdgeNote> toRemove = new ArrayList<FigEdgeNote>();
Collection c = getFigEdges(); // all connected edges
for (Iterator i = c.iterator(); i.hasNext(); ) {
FigEdgeNote fen = (FigEdgeNote) i.next();
Object otherEnd = fen.getDestination(); // the UML object
if (otherEnd == getOwner()) { // wrong end of the edge
otherEnd = fen.getSource();
}
if (otherEnd == mee.getOldValue()) {
toRemove.add(fen);
}
}
for (FigEdgeNote fen : toRemove) {
fen.removeFromDiagram();
}
}
}
/*
* @see org.argouml.uml.diagram.ui.FigNodeModelElement#updateStereotypeText()
*/
@Override
protected void updateStereotypeText() {
Object me = getOwner();
if (me == null) {
return;
}
Rectangle rect = getBounds();
Dimension stereoMin = getStereotypeFig().getMinimumSize();
if (Model.getFacade().getStereotypes(me).isEmpty()) {
if (getStereotypeFig().isVisible()) {
getStereotypeFig().setVisible(false);
rect.y += stereoMin.height;
rect.height -= stereoMin.height;
setBounds(rect.x, rect.y, rect.width, rect.height);
calcBounds();
}
} else {
if (!getStereotypeFig().isVisible()) {
getStereotypeFig().setVisible(true);
// Only adjust the stereotype height if we are not newly
// created. This gets round the problem of loading classes with
// stereotypes defined, which have the height already including
// the stereotype.
if (!newlyCreated) {
rect.y -= stereoMin.height;
rect.height += stereoMin.height;
rect.width =
Math.max(getMinimumSize().width, rect.width);
setBounds(rect.x, rect.y, rect.width, rect.height);
calcBounds();
}
}
}
// Whatever happened we are no longer newly created, so clear the
// flag. Then set the bounds for the rectangle we have defined.
newlyCreated = false;
}
/**
* Get the text body of the comment.
* @return the body of the comment
*/
public String getBody() {
return bodyTextFig.getText();
}
/*
* @see org.tigris.gef.presentation.Fig#getClosestPoint(java.awt.Point)
*/
@Override
public Point getClosestPoint(Point anotherPt) {
Rectangle r = getBounds();
int[] xs = {
r.x, r.x + r.width - dogear, r.x + r.width,
r.x + r.width, r.x, r.x,
};
int[] ys = {
r.y, r.y, r.y + dogear,
r.y + r.height, r.y + r.height, r.y,
};
Point p =
Geometry.ptClosestTo(
xs,
ys,
6,
anotherPt);
return p;
}
/**
* The UID.
*/
private static final long serialVersionUID = 7242542877839921267L;
}