| 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; |
| } |
| |
| } |