blob: 9a7761c57485feb99b07cfabbfc5e604cf6463f1 [file] [log] [blame]
package net.sf.taverna.t2.lang.uibuilder;
import static net.sf.taverna.t2.lang.uibuilder.Icons.getIcon;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import org.apache.log4j.Logger;
/**
* Superclass for list and wrapped list components generated by the UI builder.
* Accepts properties to determine whether the 'new', 'move' and 'delete'
* controls should be displayed and what, if any, concrete class should be used
* when constructing new items. Properties are as follows:
* <p>
* <ul>
* <li><code>nodelete</code> If set do not show the 'delete item' button</li>
* <li><code>nomove</code> If set do not show the 'move up' and 'move down'
* buttons</li>
* <li><code>new=some.class.Name</code> If set then use the specified class,
* loaded with the target object's classloader, when adding new items. Also adds
* the 'New Item' button to the top of the list panel. The specified class must
* be valid to insert into the list and must have a no-argument constructor</li>
* </ul>
*
* @author Tom Oinn
*
*/
public abstract class AbstractListComponent extends JPanel {
private static final long serialVersionUID = 2067559836729348490L;
private static Logger logger = Logger
.getLogger(AbstractListComponent.class);
private String fieldName;
@SuppressWarnings("unchecked")
private List theList;
private Properties props;
private Map<String, Properties> fieldProps;
private List<String> subFields;
private String parent;
private JPanel listItemContainer;
private boolean showDelete = true;
private boolean showMove = true;
@SuppressWarnings("unchecked")
private Class newItemClass = null;
protected static Color deferredButtonColour = Color.orange;
private static Object[] prototypes;
static {
try {
prototypes = new Object[] { new URL("http://some.host.com/path"),
new Boolean(true), new Integer(1), new Float(1),
new Short((short) 1), new Long((long) 1),
new Character('a'), new Double((double) 1),
new Byte((byte) 1) };
} catch (MalformedURLException e) {
logger.error("Unable to generate URL", e);
}
}
/**
* Build a generic list component
*
* @param fieldName
* the name of the field this component represents in its parent
* bean
* @param theList
* the list value of the field
* @param props
* properties for this field
* @param fieldProps
* aggregated properties for all fields in the UI builder scope
* @param newItemClass
* the class to use for construction of new item instances, or
* null if this is not supported
* @param subFields
* all field names within this field, this will be empty for a
* wrapped list
* @param parent
* the parent field ID used to access subfield properties
* @throws NoSuchMethodException
* @throws ClassNotFoundException
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws IllegalArgumentException
*/
@SuppressWarnings("unchecked")
protected AbstractListComponent(String fieldName, List theList,
Properties props, Map<String, Properties> fieldProps,
final Class<?> newItemClass, List<String> subFields, String parent)
throws NoSuchMethodException, IllegalArgumentException,
IllegalAccessException, InvocationTargetException,
ClassNotFoundException {
super();
this.fieldName = fieldName;
this.theList = theList;
this.props = props;
this.fieldProps = fieldProps;
this.subFields = subFields;
this.parent = parent;
if (props.containsKey("nodelete")) {
showDelete = false;
}
if (props.containsKey("nomove")) {
showMove = false;
}
this.newItemClass = newItemClass;
setOpaque(false);
setLayout(new BorderLayout());
String displayName = fieldName;
if (props.containsKey("name")) {
displayName = props.getProperty("name");
}
setBorder(BorderFactory.createTitledBorder(displayName));
if (newItemClass != null) {
// Generate 'add new' UI here
JPanel newItemPanel = new JPanel();
newItemPanel.setOpaque(false);
newItemPanel.setLayout(new BoxLayout(newItemPanel,
BoxLayout.LINE_AXIS));
newItemPanel.add(Box.createHorizontalGlue());
final JButton newItemButton = new JButton("New Item",
getIcon("new"));
newItemPanel.add(newItemButton);
newItemButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
try {
Object instance = null;
try {
instance = newItemClass.newInstance();
} catch (InstantiationException ie) {
// Probably because the class has no default
// constructor, use the prototype list
for (Object prototype : prototypes) {
if (newItemClass.isAssignableFrom(prototype
.getClass())) {
instance = prototype;
break;
}
}
if (instance == null) {
throw ie;
}
}
addNewItemToList(instance);
newItemButton.setForeground(BeanComponent.validColour);
} catch (Exception ex) {
newItemButton
.setForeground(BeanComponent.invalidColour);
logger.error("", ex);
}
}
});
add(newItemPanel, BorderLayout.NORTH);
}
listItemContainer = new JPanel();
listItemContainer.setOpaque(false);
listItemContainer.setLayout(new BoxLayout(listItemContainer,
BoxLayout.PAGE_AXIS));
updateListContents();
add(listItemContainer, BorderLayout.CENTER);
}
protected void updateListContents() throws NoSuchMethodException,
IllegalArgumentException, IllegalAccessException,
InvocationTargetException, ClassNotFoundException {
listItemContainer.removeAll();
boolean first = true;
int index = 0;
List<JComponent> listComponents = getListComponents();
for (JComponent component : listComponents) {
if (first && newItemClass == null) {
first = false;
} else {
List<Component> c = getSeparatorComponents();
if (c != null) {
for (Component jc : c) {
listItemContainer.add(jc);
}
}
}
JComponent wrappedComponent = wrapWithControls(component, index++,
listComponents.size());
if (wrappedComponent != null) {
listItemContainer.add(wrappedComponent);
} else {
listItemContainer.add(component);
}
}
revalidate();
}
/**
* Wrap the given component in a panel including whatever list manipulation
* controls are needed. The index of the item being wrapped is supplied to
* inform the various actions the controls can perform. By default this
* returns a JPanel with delete and move controls
*
* @param component
* @param index
* @return
*/
protected JPanel wrapWithControls(JComponent component, final int index,
int listSize) {
int numberOfButtons = 3;
if (!showDelete) {
numberOfButtons--;
}
if (!showMove) {
numberOfButtons -= 2;
}
if (numberOfButtons == 0) {
return null;
}
JPanel result = new JPanel();
result.setOpaque(false);
result.setLayout(new BorderLayout());
result.add(component, BorderLayout.CENTER);
// Construct the controls
JPanel controls = new JPanel();
controls.setOpaque(false);
controls.setLayout(new BorderLayout());
controls
.add(new JSeparator(SwingConstants.VERTICAL), BorderLayout.WEST);
result.add(controls, BorderLayout.EAST);
JPanel buttons = new JPanel();
buttons.setOpaque(false);
buttons.setLayout(new GridLayout(0, numberOfButtons));
if (showMove) {
// Move up button, or spacer if already at index 0
if (index > 0) {
JButton moveUpButton = createButton("up", false);
moveUpButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
try {
itemMoved(index, index - 1);
} catch (Exception ex) {
logger.error("Unable to move item", ex);
}
}
});
buttons.add(moveUpButton);
} else {
buttons.add(Box.createGlue());
}
// Move down button, or spacer if index == listSize-1
if (index < (listSize - 1)) {
JButton moveDownButton = createButton("down", false);
moveDownButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
try {
itemMoved(index, index + 1);
} catch (Exception ex) {
logger.error("Unable to move item", ex);
}
}
});
buttons.add(moveDownButton);
} else {
buttons.add(Box.createGlue());
}
}
if (showDelete) {
// Delete button
JButton deleteButton = createButton("delete", true);
deleteButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
try {
deleteItemAtIndex(index);
} catch (Exception ex) {
logger.error("Unable to delete item", ex);
}
}
});
buttons.add(deleteButton);
}
JPanel buttonWrapper = new JPanel();
buttonWrapper.setLayout(new BorderLayout());
buttonWrapper.setOpaque(false);
buttonWrapper.add(buttons, BorderLayout.NORTH);
controls.add(buttonWrapper, BorderLayout.CENTER);
return result;
}
private static Timer timer = new Timer();
@SuppressWarnings("serial")
private JButton createButton(String iconName, final boolean deferActions) {
JButton result = new JButton(getIcon(iconName)) {
@Override
public Dimension getPreferredSize() {
return new Dimension(getIcon().getIconWidth() + 8, (int) super
.getPreferredSize().getHeight());
}
@Override
public Dimension getMinimumSize() {
return new Dimension(getIcon().getIconWidth() + 8, (int) super
.getMinimumSize().getHeight());
}
private boolean active = false;
private Color defaultBackground = null;
@Override
public void addActionListener(final ActionListener theListener) {
if (defaultBackground == null) {
defaultBackground = getBackground();
}
if (!deferActions) {
super.addActionListener(theListener);
return;
} else {
super.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ae) {
if (active) {
theListener.actionPerformed(ae);
} else {
setActive(true);
timer.schedule(new TimerTask() {
@Override
public void run() {
SwingUtilities
.invokeLater(new Runnable() {
public void run() {
setActive(false);
}
});
}
}, 1000);
}
}
});
}
}
private synchronized void setActive(boolean isActive) {
if (isActive == active) {
return;
} else {
active = isActive;
setBackground(active ? deferredButtonColour
: defaultBackground);
}
}
};
result.setFocusable(false);
return result;
}
/**
* Called when building the UI, must return a list of editor components
* corresponding to items in the list
*
* @throws NoSuchMethodException
* @throws ClassNotFoundException
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws IllegalArgumentException
*/
protected abstract List<JComponent> getListComponents()
throws NoSuchMethodException, IllegalArgumentException,
IllegalAccessException, InvocationTargetException,
ClassNotFoundException;
/**
* Override to specify a separator component to be used inbetween internal
* list components, by default no component is used (this returns null).
*
* @return
*/
protected List<Component> getSeparatorComponents() {
return null;
}
/**
* Called when the user has clicked on the 'new item' button, this method is
* passed the new instance of the specified type and should handle the
* addition of this item to the list and the update of the UI to reflect
* this change
*
* @param o
* the object to add to the list
* @throws ClassNotFoundException
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws NoSuchMethodException
* @throws IllegalArgumentException
*/
protected abstract void addNewItemToList(Object o)
throws IllegalArgumentException, NoSuchMethodException,
IllegalAccessException, InvocationTargetException,
ClassNotFoundException;
/**
* Called when the user has clicked on the 'delete item' button next to a
* particular item in the list, this method is passed the index within the
* list of the item to be deleted and must update the UI appropriately
*
* @param index
* @throws ClassNotFoundException
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws NoSuchMethodException
* @throws IllegalArgumentException
*/
protected abstract void deleteItemAtIndex(int index)
throws IllegalArgumentException, NoSuchMethodException,
IllegalAccessException, InvocationTargetException,
ClassNotFoundException;
/**
* Called when the user has moved an item from one index to another, passed
* the old index of the item and the desired new index. This method must
* effect the actual move within the list and the update of the UI.
*
* @throws ClassNotFoundException
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws NoSuchMethodException
* @throws IllegalArgumentException
*/
protected abstract void itemMoved(int fromIndex, int toIndex)
throws IllegalArgumentException, NoSuchMethodException,
IllegalAccessException, InvocationTargetException,
ClassNotFoundException;
/**
* Get the field name of this component
*
* @return
*/
protected final String getFieldName() {
return this.fieldName;
}
/**
* Return the underlying list presented by this component
*
* @return
*/
@SuppressWarnings("unchecked")
protected final List getUnderlyingList() {
return this.theList;
}
/**
* Get a list of (renamed) sub-fields of this field, so if this field was
* foo.bar.someList and had a foo.bar.someList.urgle this would contain
* 'urgle'
*
* @return
*/
protected final List<String> getSubFields() {
return this.subFields;
}
/**
* Get the properties applied to this list component
*
* @return
*/
protected final Properties getProperties() {
return this.props;
}
/**
* The parent field name is the name of this field plus its parent string
* and is used when recursively building sub-panels within the list. Pass
* this into the 'parent' argument of the UIBuilder's construction methods.
*
* @return
*/
protected final String getParentFieldName() {
return this.parent;
}
/**
* Get the map of all field->property object block defined by the UI builder
* constructing this component. This is used along with the parent field
* name to determine properties of sub-components when recursing into a
* nested collection.
*
* @return
*/
protected final Map<String, Properties> getFieldProperties() {
return this.fieldProps;
}
}