/* $Id: UMLModelElementListModel.java 18208 2010-04-05 10:30:14Z bobtarling $
*****************************************************************************
* Copyright (c) 2009 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:
* bobtarling
*****************************************************************************
*
* Some portions of this file was previously release using the BSD License:
*/
// Copyright (c) 2002-2008 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.core.propertypanels.ui;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.swing.Action;
import javax.swing.DefaultListModel;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.MenuElement;
import javax.swing.SwingUtilities;
import org.apache.log4j.Logger;
import org.argouml.model.AddAssociationEvent;
import org.argouml.model.AssociationChangeEvent;
import org.argouml.model.AttributeChangeEvent;
import org.argouml.model.InvalidElementException;
import org.argouml.model.Model;
import org.argouml.model.RemoveAssociationEvent;
import org.argouml.uml.ui.AbstractActionAddModelElement2;
import org.argouml.uml.ui.AbstractActionNewModelElement;
import org.argouml.uml.ui.AbstractActionRemoveElement;
import org.tigris.toolbar.ToolBar;
/**
* The model for a list that contains ModelElements. The state of the Element is
* still kept in the model subsystem itself. This list is only to be used as the
* model for some GUI element like UMLLinkedList.
*
* @since Oct 2, 2002
* @author jaap.branderhorst@xs4all.nl
*/
abstract class UMLModelElementListModel
extends DefaultListModel implements PropertyChangeListener {
private static final Logger LOG =
Logger.getLogger(UMLModelElementListModel.class);
private String eventName = null;
private Object listTarget = null;
private AbstractActionAddModelElement2 addAction = null;
private AbstractActionNewModelElement newAction = null;
private AbstractActionRemoveElement removeAction = null;
private static int count = 0;
private final int instance = count++;
/**
* Flag to indicate whether list events should be fired
*/
private boolean fireListEvents = true;
/**
* Flag to indicate whether the model is being build
*/
private boolean buildingModel = false;
/**
* The type of model elements this list model is designed to hold.
*/
private Object metaType;
/**
* Indicates that drops onto this list should connect in the opposite
* way to standard.
*/
private boolean reverseDropConnection;
/**
* True if the icon for the modelelement should be shown. The icon is, for
* instance, a small class symbol for a class.
*/
final private boolean showIcon;
/**
* True if the containment path should be shown
* (to help the user disambiguate elements with the same name);
*/
final private boolean showPath;
/**
* Constructor to be used if the subclass does not depend on the
* MELementListener methods and setTarget method implemented in this
* class.
*/
UMLModelElementListModel() {
super();
showIcon = true;
showPath = true;
}
/**
* Constructor for UMLModelElementListModel2.
*
* @param name the name of the event to listen to, which triggers us
* to update the list model from the UML data
*/
UMLModelElementListModel(String name) {
super();
eventName = name;
showIcon = true;
showPath = true;
}
UMLModelElementListModel(
final String name,
final boolean showIcon,
final boolean showPath) {
super();
eventName = name;
this.showIcon = showIcon;
this.showPath = showPath;
}
UMLModelElementListModel(
final String name,
final boolean showIcon,
final boolean showPath,
final Object metaType) {
super();
eventName = name;
this.showIcon = showIcon;
this.showPath = showPath;
this.metaType = metaType;
}
/**
* Constructor for UMLModelElementListModel2.
*
* @param name the name of the event to listen to, which triggers us
* to update the list model from the UML data
* @param theMetaType the type of model element that the list model
* is designed to contain.
*/
UMLModelElementListModel(String name, Object theMetaType) {
super();
this.metaType = theMetaType;
eventName = name;
showIcon = true;
showPath = true;
}
/**
* Constructor for UMLModelElementListModel2.
*
* @param name the name of the event to listen to, which triggers us
* to update the list model from the UML data
* @param theMetaType the type of model element that the list model
* is designed to contain.
*/
public UMLModelElementListModel(
final String name,
final Object theMetaType,
final AbstractActionAddModelElement2 addAction,
final AbstractActionNewModelElement newAction,
final AbstractActionRemoveElement removeAction) {
super();
this.metaType = theMetaType;
eventName = name;
showIcon = true;
showPath = true;
this.addAction = addAction;
this.newAction = newAction;
this.removeAction = removeAction;
}
public UMLModelElementListModel(
final String name,
final Object theMetaType,
final AbstractActionAddModelElement2 addAction) {
super();
this.metaType = theMetaType;
eventName = name;
showIcon = true;
showPath = true;
this.addAction = addAction;
}
public UMLModelElementListModel(
final String name,
final Object theMetaType,
final AbstractActionNewModelElement newAction) {
super();
this.metaType = theMetaType;
eventName = name;
showIcon = true;
showPath = true;
this.newAction = newAction;
}
public UMLModelElementListModel(
final String name,
final Object theMetaType,
final AbstractActionAddModelElement2 addAction,
final AbstractActionRemoveElement removeAction) {
super();
this.metaType = theMetaType;
eventName = name;
showIcon = true;
showPath = true;
this.addAction = addAction;
this.removeAction = removeAction;
}
public UMLModelElementListModel(
final String name,
final Object theMetaType,
final AbstractActionAddModelElement2 addAction,
final AbstractActionNewModelElement newAction) {
super();
this.metaType = theMetaType;
eventName = name;
showIcon = true;
showPath = true;
this.addAction = addAction;
this.newAction = newAction;
}
/**
* Constructor for UMLModelElementListModel2.
*
* @param name the name of the event to listen to, which triggers us
* to update the list model from the UML data
* @param theMetaType the type of model element that the list model
* is designed to contain.
* @param reverseTheDropConnection tells the JList to reverse the
* connection made and drop during dnd.
*/
public UMLModelElementListModel(
String name,
Object theMetaType,
boolean reverseTheDropConnection) {
super();
this.metaType = theMetaType;
eventName = name;
this.reverseDropConnection = reverseTheDropConnection;
showIcon = true;
showPath = true;
}
/**
* Get the type of objects that this list model is designed to contain.
* @return metaType the meta type.
*/
public Object getMetaType() {
return metaType;
}
public boolean isReverseDropConnection() {
return reverseDropConnection;
}
/**
* @param building The buildingModel to set.
*/
protected void setBuildingModel(boolean building) {
this.buildingModel = building;
}
/**
* @param t the list target to set
*/
protected void setListTarget(Object t) {
this.listTarget = t;
}
/*
* @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
*
* TODO: This should be reviewed to see if it can be improved with a view
* towards removing some of the overrriding methods used as workarounds for
* differences between NSUML and MDR - tfm - 20060302
*/
public void propertyChange(final PropertyChangeEvent e) {
final UMLModelElementListModel lm = this;
Runnable doWorkRunnable = new Runnable() {
public void run() {
try {
// The original controls on property panels regularly rebuild themselves
// from scratch rather than add and remove elements
// For now we just try and add/remove but we can reinstate this if required.
//
// if (e instanceof AttributeChangeEvent) {
// try {
// if (isValidEvent(e)) {
// LOG.info("Rebuilding model");
// rebuildModelList();
// }
// } catch (InvalidElementException iee) {
// return;
// }
// } else
if (e instanceof AddAssociationEvent) {
if (isValidEvent(e)) {
Object o = getChangedElement(e);
if (o instanceof Collection) {
LOG.info("Elements added");
ArrayList tempList = new ArrayList((Collection) o);
Iterator it = tempList.iterator();
while (it.hasNext()) {
Object o2 = it.next();
addElement(o2);
}
} else {
/* TODO: If this is an ordered list, then you have to
add in the right location! */
if (!contains(o)) {
if (lm instanceof Ordered) {
Ordered ordered = (Ordered) lm;
Collection elements = ordered.getModelElements();
if (elements instanceof List) {
final int posn = ((List) elements).indexOf(o);
// We tested this above - do we need to test again?
add(posn, o);
} else {
int posn = 0;
for (Object element : elements) {
if (element == o) {
break;
}
posn++;
}
add(posn, o);
}
} else {
addElement(o);
}
}
}
}
} else if (e instanceof RemoveAssociationEvent) {
boolean valid = false;
if (!(getChangedElement(e) instanceof Collection)) {
valid = contains(getChangedElement(e));
} else {
Collection col = (Collection) getChangedElement(e);
Iterator it = col.iterator();
valid = true;
while (it.hasNext()) {
Object o = it.next();
if (!contains(o)) {
valid = false;
break;
}
}
}
if (valid) {
Object o = getChangedElement(e);
if (o instanceof Collection) {
Iterator it = ((Collection) o).iterator();
while (it.hasNext()) {
Object o3 = it.next();
removeElement(o3);
}
} else {
removeElement(o);
}
}
}
} catch (InvalidElementException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("updateLayout method accessed "
+ "deleted element ", e);
}
}
}
};
SwingUtilities.invokeLater(doWorkRunnable);
}
/**
* Delete and rebuild the model list from scratch.
*/
private void rebuildModelList() {
LOG.info("Rebuilding");
removeAllElements();
buildingModel = true;
try {
buildModelList();
} catch (InvalidElementException exception) {
/*
* This can throw an exception if the target has been
* deleted. We don't want to try locking the repository
* because this is called from the event delivery thread and
* could cause a deadlock. Instead catch the exception and
* leave the model empty.
*/
LOG.debug("buildModelList threw exception for target "
+ getTarget() + ": "
+ exception);
} finally {
buildingModel = false;
}
if (getSize() > 0) {
fireIntervalAdded(this, 0, getSize() - 1);
}
}
/**
* Builds the list of elements. Called from targetChanged every time the
* target of the proppanel is changed. Usually the method setAllElements is
* called with the result.
*/
protected abstract void buildModelList();
/**
* Utility method to set the elements of this list to the contents of the
* given collection.
* @param col the given collection
*/
protected void setAllElements(Collection col) {
if (!isEmpty())
removeAllElements();
addAll(col);
}
/**
* Utility method to add a collection of elements to the model
* @param col the given collection
*/
protected void addAll(Collection col) {
if (col.size() == 0) return;
Iterator it = col.iterator();
fireListEvents = false;
int intervalStart = getSize() == 0 ? 0 : getSize() - 1;
while (it.hasNext()) {
Object o = it.next();
addElement(o);
}
fireListEvents = true;
fireIntervalAdded(this, intervalStart, getSize() - 1);
}
/**
* Utility method to get the target. Sets the target if the target is null
* via the method setTarget().
* @return MModelElement
*/
public Object getTarget() {
return listTarget;
}
/**
* Utility method to get the changed element from some event e
* @param e the event
* @return Object the changed element
*/
protected Object getChangedElement(PropertyChangeEvent e) {
if (e instanceof AssociationChangeEvent) {
return ((AssociationChangeEvent) e).getChangedValue();
}
if (e instanceof AttributeChangeEvent) {
return ((AttributeChangeEvent) e).getSource();
}
return e.getNewValue();
}
/*
* @see javax.swing.DefaultListModel#contains(java.lang.Object)
*/
public boolean contains(Object elem) {
if (super.contains(elem)) {
return true;
}
if (elem instanceof Collection) {
Iterator it = ((Collection) elem).iterator();
while (it.hasNext()) {
if (!super.contains(it.next())) {
return false;
}
}
return true;
}
return false;
}
/**
* Sets the target. If the old target is a ModelElement, it also removes
* the model from the element listener list of the target. If the new target
* is instanceof ModelElement, the model is added as element listener to the
* new target. <p>
*
* This function is called when the user changes the target.
* Hence, this shall not result in any UML model changes.
* Hence, we block firing list events completely by setting
* buildingModel to true for the duration of this function. <p>
*
* This function looks a lot like the one in UMLComboBoxModel.
*
* @param theNewTarget the new target
*/
protected void setTarget(final Object theNewTarget) {
assert (getTarget() == null);
assert (Model.getFacade().isAUMLElement(theNewTarget));
listTarget = theNewTarget;
Model.getPump().addModelEventListener(this, listTarget, eventName);
// Allow listening to other elements:
addOtherModelEventListeners(listTarget);
rebuildModelList();
}
public void removeModelEventListener() {
Model.getPump().removeModelEventListener(this, listTarget, eventName);
}
/**
* This function allows subclasses to listen to more modelelements.
* The given target is guaranteed to be a UML modelelement.
*
* @param oldTarget the UML modelelement
*/
protected void removeOtherModelEventListeners(Object oldTarget) {
/* Do nothing by default. */
}
/**
* This function allows subclasses to listen to more modelelements.
* The given target is guaranteed to be a UML modelelement.
*
* @param newTarget the UML modelelement
*/
protected void addOtherModelEventListeners(Object newTarget) {
/* Do nothing by default. */
}
/**
* Returns true if the given element is valid, i.e. it may be added to the
* list of elements.
*
* @param element the element to be tested
* @return true if valid
*/
protected abstract boolean isValidElement(Object element);
/**
* Returns true if some event is valid. An event is valid if the
* element changed in the event is valid. This is determined via a
* call to isValidElement. This method can be overriden by
* subclasses if they cannot determine if it is a valid event just
* by checking the changed element.
*
* @param e the event
* @return boolean true if valid
*/
protected boolean isValidEvent(PropertyChangeEvent e) {
boolean valid = false;
if (!(getChangedElement(e) instanceof Collection)) {
// TODO: Considering all delete events to be valid like below
// is going to cause lots of unecessary work and some problems
if ((e.getNewValue() == null && e.getOldValue() != null)
// Don't test changed element if it was deleted
|| isValidElement(getChangedElement(e))) {
valid = true; // we tried to remove a value
}
} else {
Collection col = (Collection) getChangedElement(e);
Iterator it = col.iterator();
if (!col.isEmpty()) {
valid = true;
while (it.hasNext()) {
Object o = it.next();
if (!isValidElement(o)) {
valid = false;
break;
}
}
} else {
if (e.getOldValue() instanceof Collection
&& !((Collection) e.getOldValue()).isEmpty()) {
valid = true;
}
}
}
return valid;
}
/*
* @see javax.swing.DefaultListModel#addElement(java.lang.Object)
*/
public void addElement(Object obj) {
if (obj != null && !contains(obj)) {
super.addElement(obj);
}
}
/**
* Returns the eventName. This method is only here for testing goals.
* @return String
*/
String getEventName() {
return eventName;
}
/**
* Sets the eventName. The eventName is the name of the
* MElementEvent to which the list should listen. The list is
* registred with UMLModelEventPump and only gets events that have
* a name like eventName. This method should be called in the
* constructor of every subclass.
*
* @param theEventName The eventName to set
*/
protected void setEventName(String theEventName) {
eventName = theEventName;
}
/*
* @see javax.swing.AbstractListModel#fireContentsChanged(
* Object, int, int)
*/
protected final void fireContentsChanged(Object source, int index0, int index1) {
if (fireListEvents && !buildingModel)
super.fireContentsChanged(source, index0, index1);
}
/*
* @see javax.swing.AbstractListModel#fireIntervalAdded(
* Object, int, int)
*/
protected final void fireIntervalAdded(Object source, int index0, int index1) {
if (fireListEvents && !buildingModel)
super.fireIntervalAdded(source, index0, index1);
}
/*
* @see javax.swing.AbstractListModel#fireIntervalRemoved(
* Object, int, int)
*/
protected final void fireIntervalRemoved(Object source, int index0, int index1) {
if (fireListEvents && !buildingModel)
super.fireIntervalRemoved(source, index0, index1);
}
/**
* Override this if you want a popup menu.
* See for an example UMLModelElementOrderedListModel2.
*
* @param popup the popup menu
* @param index the selected item in the list at the moment
* the mouse was clicked
* @return true if a popup menu is created, and needs to be shown
*/
public boolean buildPopup(JPopupMenu popup, int index) {
return false;
}
public List<Action> getActions() {
final List<Action> actions = new ArrayList<Action>();
final JPopupMenu popup = new JPopupMenu();
buildPopup(popup, 0);
final MenuElement[] elements = popup.getSubElements();
for (MenuElement element : elements) {
if (element instanceof JMenuItem) {
actions.add(((JMenuItem) element).getAction());
}
}
return actions;
}
/**
* Get a toolbar containing all the actions available on this JList.
* @return the toolbar.
*/
public ToolBar getToolbar() {
// TODO: First implementation will be to call buildPopop
// and extract all the contained actions. The toolbar will
// be built to the same structure.
// The toolbar will be displayed to the left of the JList
// under the label.
return null;
}
protected boolean hasPopup() {
return false;
}
boolean isShowIcon() {
return showIcon;
}
boolean isShowPath() {
return showPath;
}
public AbstractActionAddModelElement2 getAddAction() {
return addAction;
}
public AbstractActionNewModelElement getNewAction() {
return newAction;
}
public AbstractActionRemoveElement getRemoveAction() {
return removeAction;
}
public List<Action> getNewActions() {
return Collections.emptyList();
}
}