/* $Id: FigEdgeModelElement.java 18859 2010-11-29 21:37:03Z 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: * Thomas Neustupny * Bob Tarling * 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.ui; import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.Point; import java.awt.Rectangle; 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.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.Vector; import javax.swing.Action; import javax.swing.Icon; import javax.swing.JMenu; import javax.swing.JSeparator; import javax.swing.SwingUtilities; import org.apache.log4j.Logger; import org.argouml.application.events.ArgoDiagramAppearanceEvent; import org.argouml.application.events.ArgoDiagramAppearanceEventListener; import org.argouml.application.events.ArgoEventPump; import org.argouml.application.events.ArgoEventTypes; import org.argouml.application.events.ArgoHelpEvent; import org.argouml.application.events.ArgoNotationEvent; import org.argouml.application.events.ArgoNotationEventListener; import org.argouml.cognitive.Designer; import org.argouml.cognitive.Highlightable; import org.argouml.cognitive.ToDoItem; import org.argouml.cognitive.ToDoList; import org.argouml.cognitive.ui.ActionGoToCritique; import org.argouml.i18n.Translator; import org.argouml.kernel.DelayedChangeNotify; import org.argouml.kernel.DelayedVChangeListener; import org.argouml.kernel.Owned; import org.argouml.kernel.Project; import org.argouml.model.AddAssociationEvent; import org.argouml.model.AssociationChangeEvent; import org.argouml.model.AttributeChangeEvent; import org.argouml.model.DeleteInstanceEvent; import org.argouml.model.DiElement; import org.argouml.model.InvalidElementException; import org.argouml.model.Model; import org.argouml.model.RemoveAssociationEvent; import org.argouml.model.UmlChangeEvent; import org.argouml.notation.Notation; import org.argouml.notation.NotationName; import org.argouml.notation.NotationProvider; import org.argouml.notation.NotationProviderFactory2; import org.argouml.notation.NotationRenderer; import org.argouml.notation.NotationSettings; import org.argouml.ui.ArgoJMenu; import org.argouml.ui.Clarifier; import org.argouml.ui.ContextActionFactoryManager; import org.argouml.ui.ProjectActions; import org.argouml.ui.targetmanager.TargetManager; import org.argouml.uml.StereotypeUtility; import org.argouml.uml.diagram.DiagramElement; import org.argouml.uml.diagram.DiagramSettings; import org.argouml.uml.ui.ActionDeleteModelElements; import org.argouml.util.IItemUID; import org.argouml.util.ItemUID; import org.tigris.gef.base.Geometry; import org.tigris.gef.base.Globals; import org.tigris.gef.base.Layer; import org.tigris.gef.base.Selection; import org.tigris.gef.persistence.pgml.PgmlUtility; import org.tigris.gef.presentation.Fig; import org.tigris.gef.presentation.FigCircle; import org.tigris.gef.presentation.FigEdge; import org.tigris.gef.presentation.FigEdgePoly; import org.tigris.gef.presentation.FigGroup; import org.tigris.gef.presentation.FigNode; import org.tigris.gef.presentation.FigPoly; import org.tigris.gef.presentation.FigText; /** * Abstract class to display diagram lines (edges) for UML ModelElements that * look like lines. * This Fig is prepared to show a (possibly editable) name, * and/or multiple stereotypes. * <p> * NOTE: This will drop the ArgoNotationEventListener and * ArgoDiagramAppearanceEventListener * interfaces in the next release. The corresponding methods have been marked * as deprecated. */ public abstract class FigEdgeModelElement extends FigEdgePoly implements VetoableChangeListener, DelayedVChangeListener, MouseListener, KeyListener, PropertyChangeListener, ArgoNotationEventListener, NotationRenderer, ArgoDiagramAppearanceEventListener, Highlightable, IItemUID, ArgoFig, Clarifiable, DiagramElement, Owned { private static final Logger LOG = Logger.getLogger(FigEdgeModelElement.class); private DiElement diElement = null; /** * Set the removeFromDiagram to false if this edge may not * be removed from the diagram. */ private boolean removeFromDiagram = true; /** * Offset from the end of the set of popup actions at which new items * should be inserted by concrete figures. **/ private static int popupAddOffset; private NotationProvider notationProviderName; /** * The Fig that displays the name of this model element. * Use getNameFig(), no setter should be required. */ private FigText nameFig; /** * Use getStereotypeFig(), no setter should be required. */ private FigStereotypesGroup stereotypeFig; private FigEdgePort edgePort; private ItemUID itemUid; /* * List of model element listeners we've registered. */ private Set<Object[]> listeners = new HashSet<Object[]>(); private DiagramSettings settings; /** * Construct a new FigEdge. This method creates the name element that holds * the name of the model element and adds itself as a listener. Also a * stereotype is constructed. * <p> * This constructor is only intended for use by concrete subclasses. * * @param element owning uml element * @param renderSettings rendering settings */ protected FigEdgeModelElement(Object element, DiagramSettings renderSettings) { super(); // TODO: We don't have any settings that can change per-fig currently // so we can just use the default settings; // settings = new DiagramSettings(renderSettings); settings = renderSettings; // TODO: It doesn't matter what these get set to because GEF can't // draw anything except 1 pixel wide lines super.setLineColor(LINE_COLOR); super.setLineWidth(LINE_WIDTH); getFig().setLineColor(LINE_COLOR); getFig().setLineWidth(LINE_WIDTH); nameFig = new FigNameWithAbstract(element, new Rectangle(X0, Y0 + 20, 90, 20), renderSettings, false); stereotypeFig = new FigStereotypesGroup(element, new Rectangle(X0, Y0, 90, 15), settings); initFigs(); initOwner(element); } private void initFigs() { nameFig.setTextFilled(false); setBetweenNearestPoints(true); } private void initOwner(Object element) { if (element != null) { if (!Model.getFacade().isAUMLElement(element)) { throw new IllegalArgumentException( "The owner must be a model element - got a " + element.getClass().getName()); } super.setOwner(element); if (edgePort != null) { edgePort.setOwner(getOwner()); } NotationName nn = Notation.findNotation( settings.getNotationSettings().getNotationLanguage()); notationProviderName = NotationProviderFactory2.getInstance().getNotationProvider( getNotationProviderType(), element, this, nn); addElementListener(element, "remove"); } } /** * Create a FigCommentPort if needed */ public void makeEdgePort() { if (edgePort == null) { edgePort = new FigEdgePort(getOwner(), new Rectangle(), getSettings()); edgePort.setVisible(false); addPathItem(edgePort, new PathItemPlacement(this, edgePort, 50, 0)); } } /** * @return the FigCommentPort */ public FigEdgePort getEdgePort() { return edgePort; } //////////////////////////////////////////////////////////////// // accessors /** * Setter for the UID * @param newId the new UID */ public void setItemUID(ItemUID newId) { itemUid = newId; } /** * Getter for the UID * @return the UID */ public ItemUID getItemUID() { return itemUid; } /* * @see org.tigris.gef.presentation.Fig#getTipString(java.awt.event.MouseEvent) */ @Override public String getTipString(MouseEvent me) { ToDoItem item = hitClarifier(me.getX(), me.getY()); String tip = ""; if (item != null && Globals.curEditor().getSelectionManager().containsFig(this)) { tip = item.getHeadline(); } else if (getOwner() != null) { try { tip = Model.getFacade().getTipString(getOwner()); } catch (InvalidElementException e) { // We moused over an object just as it was deleted // transient condition - doesn't require I18N LOG.warn("A deleted element still exists on the diagram"); return Translator.localize("misc.name.deleted"); } } else { tip = toString(); } if (tip != null && tip.length() > 0 && !tip.endsWith(" ")) { tip += " "; } return tip; } /** * @param me the MouseEvent that triggered the popup menu request * @return a Vector containing a combination of these 4 types: Action, * JMenu, JMenuItem, JSeparator. */ @Override public Vector getPopUpActions(MouseEvent me) { ActionList popUpActions = new ActionList(super.getPopUpActions(me), isReadOnly()); // Added this part to load the extra menu content final List<Action> modulesActions = ContextActionFactoryManager.getContextPopupActions(); for (Action a : modulesActions) { if (a instanceof List) { JMenu m = new JMenu((Action) a); popUpActions.add(m); for (Action subAction : (List<Action>) a) { m.add(subAction); } } else { popUpActions.add(a); } } // popupAddOffset should be equal to the number of items added here: popUpActions.add(new JSeparator()); popupAddOffset = 1; if (removeFromDiagram) { popUpActions.add( ProjectActions.getInstance().getRemoveFromDiagramAction()); popupAddOffset++; } popUpActions.add(new ActionDeleteModelElements()); popupAddOffset++; if (TargetManager.getInstance().getTargets().size() == 1) { ToDoList list = Designer.theDesigner().getToDoList(); List<ToDoItem> items = list.elementListForOffender(getOwner()); if (items != null && items.size() > 0) { // TODO: This creates a dependency on the Critics subsystem. // We need a generic way for modules (including our internal // subsystems) to request addition of actions to the popup // menu. - tfm 20080430 ArgoJMenu critiques = new ArgoJMenu("menu.popup.critiques"); ToDoItem itemUnderMouse = hitClarifier(me.getX(), me.getY()); if (itemUnderMouse != null) { critiques.add(new ActionGoToCritique(itemUnderMouse)); critiques.addSeparator(); } for (ToDoItem item : items) { if (item == itemUnderMouse) { continue; } critiques.add(new ActionGoToCritique(item)); } popUpActions.add(0, new JSeparator()); popUpActions.add(0, critiques); } } // Add stereotypes submenu Action[] stereoActions = getApplyStereotypeActions(); if (stereoActions != null && stereoActions.length > 0) { popUpActions.add(0, new JSeparator()); ArgoJMenu stereotypes = new ArgoJMenu( "menu.popup.apply-stereotypes"); for (int i = 0; i < stereoActions.length; ++i) { stereotypes.addCheckItem(stereoActions[i]); } popUpActions.add(0, stereotypes); } return popUpActions; } /** * Get the set of Actions which valid for adding/removing * Stereotypes on the ModelElement of this Fig's owner. * * @return array of Actions */ protected Action[] getApplyStereotypeActions() { Collection<Object> elements = new ArrayList<Object>(); Object owner = getOwner(); if (owner != null) { elements.add(owner); } for (Object o : TargetManager.getInstance().getTargets()) { Object element = null; if (Model.getFacade().isAUMLElement(o)) { element = o; } else if (o instanceof Fig) { element = ((Fig) o).getOwner(); } if (element != null && element != owner) { elements.add(element); } } return StereotypeUtility.getApplyStereotypeActions(elements); } /** * distance formula: (x-h)^2 + (y-k)^2 = distance^2 * * @param p1 point * @param p2 point * @return the square of the distance */ protected int getSquaredDistance(Point p1, Point p2) { int xSquared = p2.x - p1.x; xSquared *= xSquared; int ySquared = p2.y - p1.y; ySquared *= ySquared; return xSquared + ySquared; } /** * @param g the <code>Graphics</code> object */ public void paintClarifiers(Graphics g) { int iconPos = 25, gap = 1, xOff = -4, yOff = -4; Point p = new Point(); ToDoList tdList = Designer.theDesigner().getToDoList(); /* Owner related todo items: */ List<ToDoItem> items = tdList.elementListForOffender(getOwner()); for (ToDoItem item : items) { Icon icon = item.getClarifier(); if (icon instanceof Clarifier) { ((Clarifier) icon).setFig(this); ((Clarifier) icon).setToDoItem(item); } if (icon != null) { stuffPointAlongPerimeter(iconPos, p); icon.paintIcon(null, g, p.x + xOff, p.y + yOff); iconPos += icon.getIconWidth() + gap; } } /* Fig related todo items: */ items = tdList.elementListForOffender(this); for (ToDoItem item : items) { Icon icon = item.getClarifier(); if (icon instanceof Clarifier) { ((Clarifier) icon).setFig(this); ((Clarifier) icon).setToDoItem(item); } if (icon != null) { stuffPointAlongPerimeter(iconPos, p); icon.paintIcon(null, g, p.x + xOff, p.y + yOff); iconPos += icon.getIconWidth() + gap; } } } /** * This is used to draw a box round the edge of any editable FigText * annotations of the edge when the edge is selected. * TODO: This logic probably belongs in our base selection class * SelectionEdgeClarifiers and could be written to discover what FigText * annotations exist rather than hard code in subclasses. * @param f the fig to indicate the bounds of * @param g the graphics */ protected void indicateBounds(FigText f, Graphics g) { // No longer necessary, see issue 1048. } /** * The user clicked on the clarifier. * * @param x the x of the point clicked * @param y the y of the point clicked * @return the todo item clicked */ public ToDoItem hitClarifier(int x, int y) { int iconPos = 25, xOff = -4, yOff = -4; Point p = new Point(); ToDoList tdList = Designer.theDesigner().getToDoList(); List<ToDoItem> items = tdList.elementListForOffender(getOwner()); for (ToDoItem item : items) { Icon icon = item.getClarifier(); stuffPointAlongPerimeter(iconPos, p); int width = icon.getIconWidth(); int height = icon.getIconHeight(); if (y >= p.y + yOff && y <= p.y + height + yOff && x >= p.x + xOff && x <= p.x + width + xOff) { return item; } iconPos += width; } for (ToDoItem item : items) { Icon icon = item.getClarifier(); if (icon instanceof Clarifier) { ((Clarifier) icon).setFig(this); ((Clarifier) icon).setToDoItem(item); if (((Clarifier) icon).hit(x, y)) { return item; } } } items = tdList.elementListForOffender(this); for (ToDoItem item : items) { Icon icon = item.getClarifier(); stuffPointAlongPerimeter(iconPos, p); int width = icon.getIconWidth(); int height = icon.getIconHeight(); if (y >= p.y + yOff && y <= p.y + height + yOff && x >= p.x + xOff && x <= p.x + width + xOff) { return item; } iconPos += width; } for (ToDoItem item : items) { Icon icon = item.getClarifier(); if (icon instanceof Clarifier) { ((Clarifier) icon).setFig(this); ((Clarifier) icon).setToDoItem(item); if (((Clarifier) icon).hit(x, y)) { return item; } } } return null; } /** * @return a {@link SelectionRerouteEdge} object that manages selection and * rerouting of the edge. * * @see org.tigris.gef.presentation.Fig#makeSelection() */ @Override public Selection makeSelection() { // TODO: There is a cyclic dependency between SelectionRerouteEdge // and FigEdgeModelElement return new SelectionRerouteEdge(this); } /** * Getter for name, the name Fig * @return the nameFig */ protected FigText getNameFig() { return nameFig; } /** * Get the Rectangle in which the model elements name is displayed * * @return the bounds of the namefig */ public Rectangle getNameBounds() { return nameFig.getBounds(); } /** * @return the text of the namefig */ public String getName() { return nameFig.getText(); } /** * Getter for stereo, the stereotype Fig * @return the stereo Fig */ protected FigStereotypesGroup getStereotypeFig() { return stereotypeFig; } /* * @see java.beans.VetoableChangeListener#vetoableChange(java.beans.PropertyChangeEvent) */ public void vetoableChange(PropertyChangeEvent pce) { Object src = pce.getSource(); if (src == getOwner()) { DelayedChangeNotify delayedNotify = new DelayedChangeNotify(this, pce); SwingUtilities.invokeLater(delayedNotify); } } /* * @see org.argouml.kernel.DelayedVChangeListener#delayedVetoableChange(java.beans.PropertyChangeEvent) */ public void delayedVetoableChange(PropertyChangeEvent pce) { // update any text, colors, fonts, etc. renderingChanged(); // update the relative sizes and positions of internel Figs Rectangle bbox = getBounds(); setBounds(bbox.x, bbox.y, bbox.width, bbox.height); endTrans(); } /** * This method gets called when a bound property gets changed. This may * represent a UML element value from the Model subsystem, a GEF property, * or something which ArgoUML itself implements. * * @param pve the event containing the property change information * @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent) */ @Override public void propertyChange(final PropertyChangeEvent pve) { Object src = pve.getSource(); String pName = pve.getPropertyName(); if (pve instanceof DeleteInstanceEvent && src == getOwner()) { Runnable doWorkRunnable = new Runnable() { public void run() { try { removeFromDiagram(); } catch (InvalidElementException e) { LOG.error("updateLayout method accessed " + "deleted element", e); } } }; SwingUtilities.invokeLater(doWorkRunnable); return; } // We handle and consume editing events if (pName.equals("editing") && Boolean.FALSE.equals(pve.getNewValue())) { LOG.debug("finished editing"); // parse the text that was edited textEdited((FigText) src); calcBounds(); endTrans(); } else if (pName.equals("editing") && Boolean.TRUE.equals(pve.getNewValue())) { textEditStarted((FigText) src); } else { // Pass everything except editing events to superclass super.propertyChange(pve); } if (Model.getFacade().isAUMLElement(src) && getOwner() != null && !Model.getUmlFactory().isRemoved(getOwner())) { /* If the source of the event is an UML object, * then the UML model has been changed.*/ modelChanged(pve); final UmlChangeEvent event = (UmlChangeEvent) pve; Runnable doWorkRunnable = new Runnable() { public void run() { try { updateLayout(event); } catch (InvalidElementException e) { if (LOG.isDebugEnabled()) { LOG.debug("updateLayout method accessed " + "deleted element ", e); } } } }; SwingUtilities.invokeLater(doWorkRunnable); } /* The following is a possible future improvement * of the modelChanged() function. * Michiel: Propose not to do this to keep architecture stable. */ // if (pve instanceof AttributeChangeEvent) { // modelAttributeChanged((AttributeChangeEvent) pve); // } else if (pve instanceof AddAssociationEvent) { // modelAssociationAdded((AddAssociationEvent) pve); // } else if (pve instanceof RemoveAssociationEvent) { // modelAssociationRemoved((RemoveAssociationEvent) pve); // } } /** * Called whenever we receive an AttributeChangeEvent. * * @param ace the event */ protected void modelAttributeChanged(AttributeChangeEvent ace) { // Default implementation is to do nothing } /** * Called whenever we receive an AddAssociationEvent. * * @param aae the event */ protected void modelAssociationAdded(AddAssociationEvent aae) { // Default implementation is to do nothing } /** * Called whenever we receive an RemoveAssociationEvent. * * @param rae the event */ protected void modelAssociationRemoved(RemoveAssociationEvent rae) { // Default implementation is to do nothing } /** * This is a template method called by the ArgoUML framework as the result * of a change to a model element. Do not call this method directly * yourself. * <p>Override this in any subclasses in order to restructure the FigNode * due to change of any model element that this FigNode is listening to.</p> * <p>This method is guaranteed by the framework to be running on the * Swing/AWT thread.</p> * * @param event the UmlChangeEvent that caused the change */ protected void updateLayout(UmlChangeEvent event) { } /** * This method is called when the user doubleclicked on the text field, * and starts editing. Subclasses should override this method to e.g. * supply help to the user about the used format. <p> * * It is also possible to alter the text to be edited * already here, e.g. by adding the stereotype in front of the name, * but that seems not user-friendly. * * @param ft the FigText that will be edited and contains the start-text */ protected void textEditStarted(FigText ft) { if (ft == getNameFig()) { showHelp(notationProviderName.getParsingHelp()); ft.setText(notationProviderName.toString(getOwner(), getNotationSettings())); } } /** * Utility function to localize the given string with help text, * and show it in the status bar of the ArgoUML window. * This function is used in favour of the inline call * to enable later improvements; e.g. it would be possible to * show a help-balloon. TODO: Work this out. * One matter to possibly improve: show multiple lines. * * @param s the given string to be localized and shown */ protected void showHelp(String s) { ArgoEventPump.fireEvent(new ArgoHelpEvent( ArgoEventTypes.HELP_CHANGED, this, Translator.localize(s))); } /** * This method is called after the user finishes editing a text * field that is in the FigEdgeModelElement. Determine which field * and update the model. This class handles the name, subclasses * should override to handle other text elements. * * @param ft the text Fig that has been edited */ protected void textEdited(FigText ft) { if (ft == nameFig) { if (getOwner() == null) { return; } notationProviderName.parse(getOwner(), ft.getText()); ft.setText(notationProviderName.toString(getOwner(), getNotationSettings())); } } /** * @param f the Fig * @return true if editable */ protected boolean canEdit(Fig f) { return true; } //////////////////////////////////////////////////////////////// // event handlers - MouseListener implementation /* * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent) */ public void mousePressed(MouseEvent me) { // Required for MouseListener interface, but we only care about clicks } /* * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent) */ public void mouseReleased(MouseEvent me) { // Required for MouseListener interface, but we only care about clicks } /* * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent) */ public void mouseEntered(MouseEvent me) { // Required for MouseListener interface, but we only care about clicks } /* * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent) */ public void mouseExited(MouseEvent me) { // Required for MouseListener interface, but we only care about clicks } /* * 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. * * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent) */ public void mouseClicked(MouseEvent me) { if (!me.isConsumed() && !isReadOnly() && me.getClickCount() >= 2) { Fig f = hitFig(new Rectangle(me.getX() - 2, me.getY() - 2, 4, 4)); if (f instanceof MouseListener && canEdit(f)) { ((MouseListener) f).mouseClicked(me); } } me.consume(); } /** * Return true if the model element that this Fig represents is read only * @return The model element is read only. */ private boolean isReadOnly() { Object owner = getOwner(); if (Model.getFacade().isAUMLElement(owner)) { return Model.getModelManagementHelper().isReadOnly(owner); } return false; } /* * @see java.awt.event.KeyListener#keyPressed(java.awt.event.KeyEvent) */ public void keyPressed(KeyEvent ke) { // Required for KeyListener interface, but not used } /* * @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent) */ public void keyReleased(KeyEvent ke) { // Required for KeyListener interface, but not used } /* * @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent) */ public void keyTyped(KeyEvent ke) { if (!ke.isConsumed() && !isReadOnly() && nameFig != null && canEdit(nameFig)) { nameFig.keyTyped(ke); } } /** * Rerenders the attached elements of the fig. <p> * * Warning: The purpose of this function is NOT * to redraw the whole Fig every time * something changes. That would be inefficient.<p> * * Instead, this function should only be called * for major changes that require a complete redraw, * such as change of owner, * and change of notation language. <p> * * Overrule this function for subclasses that add extra * or remove graphical parts. */ public void renderingChanged() { // TODO: This needs to use a different method than that used by the // constructor if it wants to allow the method to be overridden initNotationProviders(getOwner()); updateNameText(); updateStereotypeText(); damage(); } //////////////////////////////////////////////////////////////// // internal methods /** * This is called after any part of the UML ModelElement has * changed. This method automatically updates the name FigText. * Subclasses should override and update other parts. * * @param e the event */ protected void modelChanged(PropertyChangeEvent e) { if (e instanceof DeleteInstanceEvent) { // No need to update if model element went away return; } if (e instanceof AssociationChangeEvent || e instanceof AttributeChangeEvent) { updateListeners(getOwner(), getOwner()); } // Update attached node figures // TODO: Presumably this should only happen on a add or remove event determineFigNodes(); } /** * generate the notation for the modelelement and stuff it into the text Fig */ protected void updateNameText() { if (getOwner() == null) { return; } if (notationProviderName != null) { String nameStr = notationProviderName.toString( getOwner(), getNotationSettings()); nameFig.setText(nameStr); updateFont(); calcBounds(); setBounds(getBounds()); } } /** * generate the notation for the stereotype and stuff it into the text Fig */ protected void updateStereotypeText() { if (getOwner() == null) { return; } Object modelElement = getOwner(); stereotypeFig.populate(); } /** * Replace the NotationProvider(s). <p> * * This method shall not be used for the initial creation of * notation providers, but only for replacing them when required. * Initialization must be done in the * constructor using methods which * can't be overridden. <p> * NotationProviders can not be updated - they * are lightweight throw-away objects. * Hence this method creates a (new) NotationProvider whenever * needed. E.g. when the notation language is * changed by the user, then the NPs are to be re-created. * So, this method shall not be * called from a Fig constructor.<p> * * After the removal of the deprecated method setOwner(), * this method shall contain the following statement: * assert notationProviderName != null * * @param own the current owner */ protected void initNotationProviders(Object own) { if (notationProviderName != null) { notationProviderName.cleanListener(); } /* This should NOT be looking for a NamedElement, * since this is not always about the name of this * modelelement alone.*/ if (Model.getFacade().isAModelElement(own)) { NotationName notation = Notation.findNotation( getNotationSettings().getNotationLanguage()); notationProviderName = NotationProviderFactory2.getInstance().getNotationProvider( getNotationProviderType(), own, this, notation); } } /** * Overrule this for subclasses * that need a different NotationProvider. * * @return the type of the notation provider */ protected int getNotationProviderType() { return NotationProviderFactory2.TYPE_NAME; } /** * Implementations of this method should register/unregister the fig for all * (model)events that may cause a repaint to be necessary. * In the simplest case, the fig should register itself * as listening to (all) events fired by (only) the owner. <p> * * But for, for example, for a * FigLink the fig must also register for events fired by the * association of the owner - because the name of * the association is shown, not the name of the Link.<p> * * In other cases, there is no need to register for any event, * e.g. when a notationProvider is used. <p> * * This function is called in 2 places: at creation (load) time of this Fig, * i.e. when the owner changes, * and in some cases by the modelChanged() function, * i.e. when the model changes. <p> * * This function shall always register for the "remove" event of the owner! * Otherwise the Fig will not be deleted when the owner gets deleted.<p> * * IF this method is called with both the oldOwner and the * newOwner equal and not null, * AND we listen only to the owner itself, * THEN we can safely ignore the call, but * ELSE we need to update the listeners of the related elements, * since the related elements may have been replaced. * * @param newOwner the new owner for the listeners, * or null if all listeners have to be removed * @param oldOwner the previous owner, * or null if there was none, and all listeners have to be set */ protected void updateListeners(Object oldOwner, Object newOwner) { Set<Object[]> l = new HashSet<Object[]>(); if (newOwner != null) { l.add(new Object[] {newOwner, "remove"}); } updateElementListeners(l); } /* * @see org.tigris.gef.presentation.Fig#setLayer(org.tigris.gef.base.Layer) */ @Override public void setLayer(Layer lay) { super.setLayer(lay); getFig().setLayer(lay); // TODO: Workaround for GEF redraw problem // Force all child figs into the same layer for (Fig f : (List<Fig>) getPathItemFigs()) { f.setLayer(lay); } } /* * @see org.tigris.gef.presentation.Fig#deleteFromModel() */ @Override public void deleteFromModel() { Object own = getOwner(); if (own != null) { getProject().moveToTrash(own); } /* TODO: MVW: Why is this not done in GEF? */ Iterator it = getPathItemFigs().iterator(); while (it.hasNext()) { ((Fig) it.next()).deleteFromModel(); } super.deleteFromModel(); } /** * @see org.argouml.application.events.ArgoNotationEventListener#notationChanged(org.argouml.application.events.ArgoNotationEvent) * @deprecated for 0.27.2 by tfmorris. Changes to notation provider are * now handled by the owning diagram. */ @Deprecated public void notationChanged(ArgoNotationEvent event) { if (getOwner() == null) { return; } renderingChanged(); } /** * @see org.argouml.application.events.ArgoNotationEventListener#notationAdded(org.argouml.application.events.ArgoNotationEvent) * @deprecated for 0.27.2 by tfmorris. */ @Deprecated public void notationAdded(ArgoNotationEvent event) { // Default implementation is to do nothing } /** * @see org.argouml.application.events.ArgoNotationEventListener#notationRemoved(org.argouml.application.events.ArgoNotationEvent) * @deprecated for 0.27.2 by tfmorris. */ @Deprecated public void notationRemoved(ArgoNotationEvent event) { // Default implementation is to do nothing } /** * @see org.argouml.application.events.ArgoNotationEventListener#notationProviderAdded(org.argouml.application.events.ArgoNotationEvent) * @deprecated for 0.27.2 by tfmorris. */ @Deprecated public void notationProviderAdded(ArgoNotationEvent event) { // Default implementation is to do nothing } /** * @see org.argouml.application.events.ArgoNotationEventListener#notationProviderRemoved(org.argouml.application.events.ArgoNotationEvent) * @deprecated for 0.27.2 by tfmorris. */ @Deprecated public void notationProviderRemoved(ArgoNotationEvent event) { // Default implementation is to do nothing } /* * @see org.tigris.gef.presentation.Fig#hit(java.awt.Rectangle) */ @Override public boolean hit(Rectangle r) { // Check if labels etc have been hit // Apparently GEF does require PathItems to be "annotations" // which ours aren't, so until that is resolved... Iterator it = getPathItemFigs().iterator(); while (it.hasNext()) { Fig f = (Fig) it.next(); if (f.hit(r)) { return true; } } return super.hit(r); } /* * @see org.tigris.gef.presentation.Fig#removeFromDiagram() */ @Override public final void removeFromDiagram() { Fig delegate = getRemoveDelegate(); // TODO: Dependency cycle between FigNodeModelElement and FigEdgeME // Is this needed? If so, introduce a Removable interface to decouple if (delegate instanceof FigNodeModelElement) { ((FigNodeModelElement) delegate).removeFromDiagramImpl(); } else if (delegate instanceof FigEdgeModelElement) { ((FigEdgeModelElement) delegate).removeFromDiagramImpl(); } else if (delegate != null) { removeFromDiagramImpl(); } } /** * Subclasses should override this to redirect a remove request from * one Fig to another. * e.g. FigEdgeAssociationClass uses this to delegate the remove to * its attached FigAssociationClass. * @return the fig handling the remove */ protected Fig getRemoveDelegate() { return this; } protected void removeFromDiagramImpl() { Object o = getOwner(); if (o != null) { removeElementListener(o); } if (notationProviderName != null) { notationProviderName.cleanListener(); } /* TODO: MVW: Why is this not done in GEF? */ Iterator it = getPathItemFigs().iterator(); while (it.hasNext()) { Fig fig = (Fig) it.next(); fig.removeFromDiagram(); } super.removeFromDiagram(); damage(); } protected void superRemoveFromDiagram() { super.removeFromDiagram(); } /* * @see org.tigris.gef.presentation.Fig#damage() */ @Override public void damage() { super.damage(); getFig().damage(); } /** * <p>Determines if the FigEdge is currently connected to the correct * FigNodes, if not the edges is the correct FigNodes set and the edge * rerouted. * <p>Typically this is used when a user has amended from the property * panel a relationship from one model element to another and the graph * needs to react to that change. * <p>e.g. if the participant of an association end is changed. * <p>Calls a helper method (layoutThisToSelf) to avoid this edge * disappearing if the new source and dest are the same node. * * TODO: This method is called far too frequently. It should only be called * when a specific event is received. It seems to be currently called whenever * any event is received from the owner. * * @return boolean whether or not the update was sucessful */ protected boolean determineFigNodes() { Object owner = getOwner(); if (owner == null) { LOG.error("The FigEdge has no owner"); return false; } if (getLayer() == null) { LOG.error("The FigEdge has no layer"); return false; } Object newSource = getSource(); Object newDest = getDestination(); Fig currentSourceFig = getSourceFigNode(); Fig currentDestFig = getDestFigNode(); Object currentSource = null; Object currentDestination = null; if (currentSourceFig != null && currentDestFig != null) { currentSource = currentSourceFig.getOwner(); currentDestination = currentDestFig.getOwner(); } if (newSource != currentSource || newDest != currentDestination) { Fig newSourceFig = getNoEdgePresentationFor(newSource); Fig newDestFig = getNoEdgePresentationFor(newDest); if (newSourceFig != currentSourceFig) { setSourceFigNode((FigNode) newSourceFig); setSourcePortFig(newSourceFig); } if (newDestFig != currentDestFig) { setDestFigNode((FigNode) newDestFig); setDestPortFig(newDestFig); } ((FigNode) newSourceFig).updateEdges(); ((FigNode) newDestFig).updateEdges(); calcBounds(); // adapted from SelectionWButtons from line 280 // calls a helper method to avoid this edge disappearing // if the new source and dest are the same node. if (newSourceFig == newDestFig) { layoutThisToSelf(); } } return true; } /** * A version of GEF's presentationFor() method which * @param element ModelElement to return presentation for * @return the Fig representing the presentation */ private Fig getNoEdgePresentationFor(Object element) { if (element == null) { throw new IllegalArgumentException("Can't search for a null owner"); } List contents = PgmlUtility.getContentsNoEdges(getLayer()); int figCount = contents.size(); for (int figIndex = 0; figIndex < figCount; ++figIndex) { Fig fig = (Fig) contents.get(figIndex); if (fig.getOwner() == element) { return fig; } } throw new IllegalStateException("Can't find a FigNode representing " + Model.getFacade().getName(element)); } /** * helper method for updateClassifiers() in order to automatically * layout an edge that is now from and to the same node type. * <p>adapted from SelectionWButtons from line 280 */ private void layoutThisToSelf() { FigPoly edgeShape = new FigPoly(); //newFC = _content; Point fcCenter = new Point(getSourceFigNode().getX() / 2, getSourceFigNode().getY() / 2); Point centerRight = new Point( (int) (fcCenter.x + getSourceFigNode().getSize().getWidth() / 2), fcCenter.y); int yoffset = (int) ((getSourceFigNode().getSize().getHeight() / 2)); edgeShape.addPoint(fcCenter.x, fcCenter.y); edgeShape.addPoint(centerRight.x, centerRight.y); edgeShape.addPoint(centerRight.x + 30, centerRight.y); edgeShape.addPoint(centerRight.x + 30, centerRight.y + yoffset); edgeShape.addPoint(centerRight.x, centerRight.y + yoffset); // place the edge on the layer and update the diagram this.setBetweenNearestPoints(true); edgeShape.setLineColor(LINE_COLOR); edgeShape.setFilled(false); edgeShape.setComplete(true); this.setFig(edgeShape); } /** * Returns the source of the edge. The source is the owner of the * node the edge travels from in a binary relationship. For * instance: for a classifierrole, this is the sender. * @return a model element */ protected Object getSource() { Object owner = getOwner(); if (owner != null) { return Model.getCoreHelper().getSource(owner); } return null; } /** * Get the model element at the source end of the edge. This is not the * same as the owner of the node at the source end, rather it is the * element that connects the element of the edge to the element of the * node. * Mostly this returns null as the edge connects directly to the node but * implementations such as the Fig for association will return an * association end that connects the association to the classifier. * @return the model element that connects the edge to the node (or null * if the edge requires no such connector. */ public Object getSourceConnector() { return null; } /** * Get the model element at the destination end of the edge. This is not * the same as the owner of the node at the source end, rather it is the * element that connects the element of the edge to the element of the * node. * Mostly this returns null as the edge connects directly to the node but * implementations such as the Fig for association will return an * association end that connects the association to the classifier. * @return the model element that connects the edge to the node (or null * if the edge requires no such connector. */ public Object getDestinationConnector() { return null; } /** * Returns the destination of the edge. The destination is the * owner of the node the edge travels to in a binary * relationship. For instance: for a classifierrole, this is the * receiver. * @return a model element */ protected Object getDestination() { Object owner = getOwner(); if (owner != null) { return Model.getCoreHelper().getDestination(owner); } return null; } /** * @param allowed true if the function RemoveFromDiagram is allowed */ protected void allowRemoveFromDiagram(boolean allowed) { this.removeFromDiagram = allowed; } /** * Set the associated Diagram Interchange element. * * @param element the element to be associated with this Fig */ public void setDiElement(DiElement element) { this.diElement = element; } /** * @return the Diagram Interchange element associated with this Fig */ public DiElement getDiElement() { return diElement; } /** * @return Returns the popupAddOffset. */ protected static int getPopupAddOffset() { return popupAddOffset; } /** * Add an element listener and remember the registration. * * @param element * element to listen for changes on * @see org.argouml.model.ModelEventPump#addModelEventListener(PropertyChangeListener, Object, String) */ protected void addElementListener(Object element) { listeners.add(new Object[] {element, null}); Model.getPump().addModelEventListener(this, element); } /** * Add a listener and remember the registration. * * @param element * element to listen for changes on * @param property * name of property to listen for changes of * @see org.argouml.model.ModelEventPump#addModelEventListener(PropertyChangeListener, Object, String) */ protected void addElementListener(Object element, String property) { listeners.add(new Object[] {element, property}); Model.getPump().addModelEventListener(this, element, property); } /** * Add a listener and remember the registration. * * @param element * element to listen for changes on * @param property * array of property names (Strings) to listen for changes of * @see org.argouml.model.ModelEventPump#addModelEventListener(PropertyChangeListener, Object, String) */ protected void addElementListener(Object element, String[] property) { listeners.add(new Object[] {element, property}); Model.getPump().addModelEventListener(this, element, property); } /** * Add an element listener and remember the registration. * * @param element * element to listen for changes on * @see org.argouml.model.ModelEventPump#addModelEventListener(PropertyChangeListener, Object, String) */ protected void removeElementListener(Object element) { listeners.remove(new Object[] {element, null}); Model.getPump().removeModelEventListener(this, element); } /** * Unregister all listeners registered through addElementListener * @see #addElementListener(Object, String) */ protected void removeAllElementListeners() { removeElementListeners(listeners); } private void removeElementListeners(Set<Object[]> listenerSet) { for (Object[] listener : listenerSet) { Object property = listener[1]; if (property == null) { Model.getPump().removeModelEventListener(this, listener[0]); } else if (property instanceof String[]) { Model.getPump().removeModelEventListener(this, listener[0], (String[]) property); } else if (property instanceof String) { Model.getPump().removeModelEventListener(this, listener[0], (String) property); } else { throw new RuntimeException( "Internal error in removeAllElementListeners"); } } listeners.removeAll(listenerSet); } private void addElementListeners(Set<Object[]> listenerSet) { for (Object[] listener : listenerSet) { Object property = listener[1]; if (property == null) { addElementListener(listener[0]); } else if (property instanceof String[]) { addElementListener(listener[0], (String[]) property); } else if (property instanceof String) { addElementListener(listener[0], (String) property); } else { throw new RuntimeException( "Internal error in addElementListeners"); } } } /** * Update the set of registered listeners to match the given set using * a minimal update strategy to remove unneeded listeners and add new * listeners. * * @param listenerSet a set of arrays containing a tuple of a UML element * to be listened to and a set of property to be listened for. */ protected void updateElementListeners(Set<Object[]> listenerSet) { Set<Object[]> removes = new HashSet<Object[]>(listeners); removes.removeAll(listenerSet); removeElementListeners(removes); Set<Object[]> adds = new HashSet<Object[]>(listenerSet); adds.removeAll(listeners); addElementListeners(adds); } /** * This optional method is not implemented. It will throw an * {@link UnsupportedOperationException} if used. Figs are * added to a GraphModel which is, in turn, owned by a project. * @param project the project * @deprecated */ @SuppressWarnings("deprecation") @Deprecated public void setProject(Project project) { throw new UnsupportedOperationException(); } /** * @deprecated for 0.27.2 by tfmorris. Implementations should have all * the information that they require in the DiagramSettings object. * * @return the owning project * @see org.argouml.uml.diagram.ui.ArgoFig#getProject() */ @Deprecated public Project getProject() { return ArgoFigUtil.getProject(this); } /** * Handles diagram font changing. * * @param e the event * @see org.argouml.application.events.ArgoDiagramAppearanceEventListener#diagramFontChanged(org.argouml.application.events.ArgoDiagramAppearanceEvent) * @deprecated for 0.27.2 by tfmorris. Global rendering changes are now * managed at the diagram level. */ @Deprecated public void diagramFontChanged(ArgoDiagramAppearanceEvent e) { updateFont(); calcBounds(); //TODO: Does this help? redraw(); } /** * This function should, for all FigTexts, * recalculate the font-style (plain, bold, italic, bold/italic), * and apply it by calling FigText.setFont(). */ protected void updateFont() { int style = getNameFigFontStyle(); Font f = getSettings().getFont(style); nameFig.setFont(f); deepUpdateFont(this); } /** * Determines the font style based on the UML model. * Overrule this in Figs that have to show bold or italic based on the * UML model they represent. * E.g. abstract classes show their name in italic. * * @return the font style for the nameFig. */ protected int getNameFigFontStyle() { return Font.PLAIN; } private void deepUpdateFont(FigEdge fe) { Font f = getSettings().getFont(Font.PLAIN); for (Object pathFig : fe.getPathItemFigs()) { deepUpdateFontRecursive(f, pathFig); } fe.calcBounds(); } /** * Changes the font for all Figs contained in the given FigGroup. <p> * * TODO: In fact, there is a design error in this method: * E.g. for a class, if the name is Italic since the class is abstract, * then the classes features should be in Plain font. * This problem can be fixed by implementing * the updateFont() method in all subclasses. * * @param fg the FigGroup to change the font of. */ private void deepUpdateFontRecursive(Font f, Object pathFig) { if (pathFig instanceof ArgoFigText) { ((ArgoFigText) pathFig).updateFont(); } else if (pathFig instanceof FigText) { ((FigText) pathFig).setFont(f); } else if (pathFig instanceof FigGroup) { for (Object fge : ((FigGroup) pathFig).getFigs()) { deepUpdateFontRecursive(f, fge); } } } public DiagramSettings getSettings() { // TODO: This is a temporary crutch to use until all Figs are updated // to use the constructor that accepts a DiagramSettings object if (settings == null) { LOG.debug("Falling back to project-wide settings"); Project p = getProject(); if (p != null) { return p.getProjectSettings().getDefaultDiagramSettings(); } } return settings; } public void setSettings(DiagramSettings renderSettings) { settings = renderSettings; renderingChanged(); } /** * @return the current notation settings */ protected NotationSettings getNotationSettings() { return getSettings().getNotationSettings(); } // public void setLineWidth(int w) { // super.setLineWidth(w); // } public void setLineColor(Color c) { super.setLineColor(c); } public void setFig(Fig f) { super.setFig(f); // GEF sets a different Fig than the one that we had at construction // time, so we need to set its color and width f.setLineColor(getLineColor()); f.setLineWidth(getLineWidth()); } /** * Setting the owner of the Fig must be done in the constructor and not * changed afterwards for all ArgoUML figs. * * @param owner owning UML element * @see org.tigris.gef.presentation.Fig#setOwner(java.lang.Object) * @throws UnsupportedOperationException * @deprecated for 0.27.3 by tfmorris. Set owner in constructor. This method * is implemented in GEF, so we'll leave this implementation * here to block any attempts to use it within ArgoUML. */ @SuppressWarnings("deprecation") @Deprecated public void setOwner(Object owner) { if (owner != getOwner()) { throw new UnsupportedOperationException( "Owner must be set in constructor and left unchanged"); } } /** * We override GEF completely here (no call to super method). * Code is unfortunately copied from GEF to avoid multiple calls * to calcBounds() * * @see org.tigris.gef.presentation.FigEdgePoly#computeRouteImpl() */ public void computeRouteImpl() { Fig sourcePortFig = getSourcePortFig(); Fig destPortFig = getDestPortFig(); if (sourcePortFig instanceof FigNodeModelElement) { sourcePortFig = ((FigNodeModelElement) sourcePortFig).getBigPort(); } if (destPortFig instanceof FigNodeModelElement) { destPortFig = ((FigNodeModelElement) destPortFig).getBigPort(); } if (!(sourcePortFig instanceof FigCircle) || !(destPortFig instanceof FigCircle)) { // If this is not a circle to circle edge we default to let GEF // calculate edge route. super.computeRouteImpl(); } else { // If the edge is from a circle to a circle (e.g. between use // cases) we have our own implementation overriding GEF. Which // attempts to keep the edges perpendicular if the edge is already // close to perpendicular. if (!_initiallyLaidOut) { layoutEdge(); _initiallyLaidOut = true; } FigPoly p = ((FigPoly) getFig()); Point srcPt = sourcePortFig.getCenter(); Point dstPt = destPortFig.getCenter(); if (_useNearest) { if (p.getNumPoints() == 2) { // ? two iterations of refinement, maybe should be a for-loop srcPt = sourcePortFig.connectionPoint(p.getPoint(1)); dstPt = destPortFig.connectionPoint(p .getPoint(p.getNumPoints() - 2)); srcPt = sourcePortFig.connectionPoint(dstPt); dstPt = destPortFig.connectionPoint(srcPt); // If the line angle is less than 3 degrees then snap the line // straight final int delta = 3; double angle = Geometry.segmentAngle(srcPt, dstPt); double mod = angle % 90; final boolean snapStraight = (mod != 0 && (mod < delta || mod > 90 - delta)); if (snapStraight) { int newX = (srcPt.x + dstPt.x) / 2; int newY = (srcPt.y + dstPt.y) / 2; if (newX < getSourcePortFig().getX() + getSourcePortFig().getWidth() && newX >= getSourcePortFig().getX()) { srcPt.x = newX; dstPt.x = newX; } else if (newY >= getSourcePortFig().getY() && newY < getSourcePortFig().getY() + getSourcePortFig().getHeight()) { srcPt.y = newY; dstPt.y = newY; } } } else { srcPt = sourcePortFig.connectionPoint(p.getPoint(1)); dstPt = destPortFig.connectionPoint(p .getPoint(p.getNumPoints() - 2)); } } setEndPoints(srcPt, dstPt); calcBounds(); } } /* end computeRoute */ public void notationRenderingChanged(NotationProvider np, String rendering) { if (notationProviderName == np) { nameFig.setText(rendering); damage(); } } public NotationSettings getNotationSettings(NotationProvider np) { if (notationProviderName == np) { return getNotationSettings(); } return null; } public Object getOwner(NotationProvider np) { if (notationProviderName == np) { return getOwner(); } return null; } }