| package net.sf.taverna.t2.lang.uibuilder; |
| |
| import java.awt.BorderLayout; |
| import java.awt.Color; |
| import java.awt.Dimension; |
| import java.beans.PropertyChangeEvent; |
| import java.beans.PropertyChangeListener; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.util.Properties; |
| |
| import javax.swing.Box; |
| import javax.swing.JLabel; |
| import javax.swing.JPanel; |
| import javax.swing.SwingUtilities; |
| import javax.swing.event.AncestorEvent; |
| import javax.swing.event.AncestorListener; |
| |
| import org.apache.log4j.Logger; |
| |
| /** |
| * Superclass of all bean component editors acting on a single (non collection) |
| * property within a POJO. This handles bound properties, using reflection to |
| * determine whether an appropriate addPropertyChangeListener method exists on |
| * the target bean and registering a listener if so. It also registers an |
| * ancestor listener to de-register this property listener if the component is |
| * removed from the display heirarchy and to re-attach it if the component is |
| * added. Implement updateComponent with the code to update the UI state from |
| * the underlying object, this is called when a property change is received by |
| * the listener (but not used for unbound properties). |
| * <p> |
| * This superclass also defines the name property: |
| * <ul> |
| * <li><code>name=SomeName</code> by default field labels use the field name |
| * defined in the configuration, including any case used. If this property is |
| * defined then the specified name is used instead</li> |
| * </ul> |
| * |
| * @author Tom Oinn |
| */ |
| public abstract class BeanComponent extends JPanel { |
| |
| private static Logger logger = Logger |
| .getLogger(BeanComponent.class); |
| |
| private static final long serialVersionUID = -6044009506938335937L; |
| protected Object target; |
| protected String propertyName; |
| protected Method getMethod, setMethod = null; |
| protected boolean editable = true; |
| protected Class<?> propertyType; |
| protected static Color invalidColour = Color.red; |
| protected static Color validColour = Color.black; |
| protected static Color uneditedColour = Color.white; |
| protected static Color editedColour = new Color(255, 245, 200); |
| protected boolean currentValueValid = true; |
| protected Object currentObjectValue = null; |
| protected JLabel label; |
| private boolean useLabel = false; |
| protected static int height = 16; |
| private Properties properties; |
| private PropertyChangeListener propertyListener = null; |
| |
| /** |
| * Equivalent to BeanComponent(target, propertyName, true, props) |
| */ |
| public BeanComponent(Object target, String propertyName, Properties props) |
| throws NoSuchMethodException { |
| this(target, propertyName, true, props); |
| } |
| |
| /** |
| * Superclass constructor for BeanComponent instances. |
| * |
| * @param target |
| * the object containing the property this component acts as a |
| * view and controller for |
| * @param propertyName |
| * name of the property in the target object |
| * @param useLabel |
| * whether to show the label component (we set this to false for |
| * wrapped lists, for example) |
| * @param props |
| * a component specific properties object, passing in any |
| * properties defined in the configuration passed to UIBuilder |
| * @throws NoSuchMethodException |
| * if the appropriate get method for the named property can't be |
| * found |
| */ |
| public BeanComponent(Object target, String propertyName, boolean useLabel, |
| Properties props) throws NoSuchMethodException { |
| super(); |
| setOpaque(false); |
| // Find methods |
| this.properties = props; |
| this.useLabel = useLabel; |
| this.target = target; |
| this.propertyName = propertyName; |
| |
| // If the target implements property change support then we can attach a |
| // listener to update the UI if the bound property changes. This |
| // listener is attached and detached in response to an ancestor listener |
| // so the bound property is only monitored when the component is visible |
| // in the UI. |
| try { |
| target.getClass().getMethod("addPropertyChangeListener", |
| String.class, PropertyChangeListener.class); |
| setUpPropertyListener(); |
| addAncestorListener(new AncestorListener() { |
| public void ancestorAdded(AncestorEvent event) { |
| setUpPropertyListener(); |
| } |
| |
| public void ancestorMoved(AncestorEvent event) { |
| // Ignore |
| } |
| |
| public void ancestorRemoved(AncestorEvent event) { |
| tearDownPropertyListener(); |
| } |
| }); |
| } catch (NoSuchMethodException nsme) { |
| // Means we don't have a bound property listener |
| } |
| |
| getMethod = findMethodWithPrefix("get"); |
| try { |
| setMethod = findMethodWithPrefix("set"); |
| } catch (NoSuchMethodException nsme) { |
| logger.error("Unable to find set method", nsme); |
| editable = false; |
| } |
| propertyType = getPropertyType(target, propertyName); |
| } |
| |
| /** |
| * Attempts to create and bind a property change listener to the target |
| * bean, failing silently if the bean doesn't implement the appropriate |
| * methods |
| */ |
| private synchronized void setUpPropertyListener() { |
| if (propertyListener == null) { |
| Method addListener = null; |
| try { |
| addListener = target.getClass().getMethod( |
| "addPropertyChangeListener", String.class, |
| PropertyChangeListener.class); |
| } catch (NoSuchMethodException nsme) { |
| return; |
| } |
| propertyListener = new PropertyChangeListener() { |
| public void propertyChange(PropertyChangeEvent evt) { |
| Object newValue = evt.getNewValue(); |
| if (currentObjectValue == null |
| || (newValue != currentObjectValue && !newValue |
| .equals(currentObjectValue))) { |
| // System.out.println("Property change, source was "+evt.getSource()); |
| if (SwingUtilities.isEventDispatchThread()) { |
| updateComponent(); |
| } else { |
| // try { |
| SwingUtilities.invokeLater(new Runnable() { |
| public void run() { |
| updateComponent(); |
| } |
| }); |
| } |
| } |
| } |
| }; |
| try { |
| addListener.invoke(target, propertyName, propertyListener); |
| } catch (IllegalArgumentException e) { |
| logger.error("Unable to set up property listener", e); |
| } catch (IllegalAccessException e) { |
| logger.error("Unable to set up property listener", e); |
| } catch (InvocationTargetException e) { |
| logger.error("Unable to set up property listener", e); |
| } |
| } |
| } |
| |
| /** |
| * If the property listener for bound properties exists then this method |
| * unregisters it from the target, attempting to use a variety of the |
| * standard method names to do so |
| */ |
| private synchronized void tearDownPropertyListener() { |
| if (propertyListener == null) { |
| return; |
| } |
| try { |
| Method removeListener = null; |
| try { |
| removeListener = target.getClass().getMethod( |
| "removePropertyChangeListener", String.class, |
| PropertyChangeListener.class); |
| removeListener.invoke(target, propertyName, propertyListener); |
| } catch (NoSuchMethodException nsme) { |
| try { |
| removeListener = target.getClass().getMethod( |
| "removePropertyChangeListener", |
| PropertyChangeListener.class); |
| removeListener.invoke(target, propertyListener); |
| } catch (NoSuchMethodException nsme2) { |
| return; |
| } |
| } |
| } catch (IllegalArgumentException e) { |
| logger.error("Unable to remove property listener", e); |
| } catch (IllegalAccessException e) { |
| logger.error("Unable to remove property listener", e); |
| } catch (InvocationTargetException e) { |
| logger.error("Unable to remove property listener", e); |
| } |
| propertyListener = null; |
| } |
| |
| /** |
| * Called by the bound property listener, implementing components must |
| * update their UI state in this method to match the value of the underlying |
| * component. This method is always called in the AWT dispatch thread so |
| * implementations can safely modify the state of UI components without |
| * worrying about swing thread handling |
| */ |
| protected abstract void updateComponent(); |
| |
| /** |
| * Adds the label to the component if labels are enabled |
| */ |
| protected void addLabel() { |
| if (useLabel) { |
| String labelName = propertyName; |
| if (getProperties().containsKey("name")) { |
| labelName = getProperties().getProperty("name"); |
| } |
| label = new JLabel(labelName); |
| label.setOpaque(false); |
| label.setPreferredSize(new Dimension( |
| label.getPreferredSize().width, height)); |
| JPanel labelPanel = new JPanel(); |
| labelPanel.setOpaque(false); |
| labelPanel.add(label); |
| labelPanel.add(Box.createHorizontalStrut(5)); |
| add(labelPanel, BorderLayout.WEST); |
| } |
| } |
| |
| /** |
| * Return the type of the property on the target object, trying to locate a |
| * matching 'get' method for the supplied property name and returning the |
| * class of that method's return type. |
| * <p> |
| * Attempts to, in order : |
| * <ol> |
| * <li>Call the method and get the concrete type of the returned value</li> |
| * <li>Get the declared return type of the get method</li> |
| * </ol> |
| * |
| * @param target |
| * @param propertyName |
| * @return |
| * @throws NoSuchMethodException |
| * if the get method can't be found for the given property name |
| * on the target |
| */ |
| public static Class<?> getPropertyType(Object target, String propertyName) |
| throws NoSuchMethodException { |
| Method getMethod = findMethodWithPrefix("get", target, propertyName); |
| try { |
| Object value = getMethod.invoke(target); |
| if (value != null) { |
| return value.getClass(); |
| } |
| } catch (InvocationTargetException ite) { |
| // |
| } catch (IllegalArgumentException e) { |
| // |
| } catch (IllegalAccessException e) { |
| // |
| } |
| // if (target instanceof ListHandler.ListItem) { |
| // return ((ListHandler.ListItem) target).getTargetClass(); |
| // } |
| return getMethod.getReturnType(); |
| } |
| |
| /** |
| * Searches for the specified method on the target object, throwing an |
| * exception if it can't be found. Searches for |
| * target.[prefix][propertyname]() |
| * |
| * @throws NoSuchMethodException |
| */ |
| static Method findMethodWithPrefix(String prefix, Object target, |
| String propertyName) throws NoSuchMethodException { |
| for (Method m : target.getClass().getMethods()) { |
| if (m.getName().equalsIgnoreCase(prefix + propertyName)) { |
| return m; |
| } |
| } |
| throw new NoSuchMethodException("Can't find method matching '" + prefix |
| + propertyName + "' in " + target.getClass().getCanonicalName()); |
| } |
| |
| /** |
| * Calls the static findMethodWithPrefix passing in the property name and |
| * the target assigned to this instance |
| * |
| * @param prefix |
| * a string prefix to use when finding the name, searches for |
| * [prefix][propertyname]() |
| * @return a Method matching the query |
| * @throws NoSuchMethodException |
| * if the method can't be found. |
| */ |
| protected final Method findMethodWithPrefix(String prefix) |
| throws NoSuchMethodException { |
| return findMethodWithPrefix(prefix, target, propertyName); |
| } |
| |
| /** |
| * Returns the toString value of the current bean property, or the empty |
| * string if the get method returns null |
| */ |
| protected final String getPropertyAsString() { |
| Object value = getProperty(); |
| if (value == null) { |
| return ""; |
| } |
| currentObjectValue = value; |
| return value.toString(); |
| } |
| |
| /** |
| * Uses reflection to call the get[property name] method on the target bean |
| * and returns the result |
| * |
| * @return current value of the bean property |
| */ |
| protected final Object getProperty() { |
| try { |
| Object value = getMethod.invoke(target); |
| return value; |
| } catch (Exception ex) { |
| logger.error("Unable to get property", ex); |
| return null; |
| } |
| } |
| |
| /** |
| * Sets the property on the object to the current object value for this UI |
| * component, effectively pushing any changes into the underlying target |
| * bean |
| */ |
| protected final void setProperty() { |
| if (currentValueValid && editable) { |
| if (setMethod != null) { |
| try { |
| setMethod.invoke(target, currentObjectValue); |
| } catch (IllegalArgumentException e) { |
| logger.error("Unable to set property", e); |
| } catch (IllegalAccessException e) { |
| logger.error("Unable to set property", e); |
| } catch (InvocationTargetException e) { |
| logger.error("Unable to set property", e); |
| } |
| } |
| } |
| } |
| |
| /** |
| * If using labels (as determined by the useLabel property) this sets the |
| * preferred width of the label for this component |
| * |
| * @param newWidth |
| * new label width in pixels |
| */ |
| public void setLabelWidth(int newWidth) { |
| if (useLabel) { |
| label.setPreferredSize(new Dimension(newWidth, height)); |
| } |
| } |
| |
| /** |
| * If using labels (as determined by the useLabel property) this returns the |
| * current preferred size of the label associated with this component |
| * |
| * @return label width in pixels |
| */ |
| public int getLabelWidth() { |
| if (useLabel) { |
| return this.label.getPreferredSize().width; |
| } else { |
| return 0; |
| } |
| } |
| |
| /** |
| * If using the label then set its text (foreground) colour |
| * |
| * @param colour |
| */ |
| public void setLabelColour(Color colour) { |
| if (useLabel) { |
| this.label.setForeground(colour); |
| } |
| } |
| |
| /** |
| * Get the properties object associated with this component. This is |
| * generated from the configuration passed to UIBuilder |
| * |
| * @return a Properties object containing named properties applying to this |
| * component in the UI |
| */ |
| public Properties getProperties() { |
| return this.properties; |
| } |
| } |