Writing JavaBean Property Editors
by Morgan Kinne


Listing One
package mybeans;
import java.awt.*;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.beans.VetoableChangeSupport;

import java.io.Serializable;

// CaseAwareTextDisplay
public class CaseAwareTextDisplay extends Canvas implements Serializable
{
  //--------------------------------------------------------------------
  // Class constants
  public static final int AS_IS         = 0;
  public static final int UPPERCASE     = 1;
  public static final int LOWERCASE     = 2;
  public static final int FIRST_IN_CAPS = 3;

  public static final String TEXT         = "text";
  public static final String TOP_MARGIN   = "topMargin";
  public static final String LEFT_MARGIN  = "leftMargin";
  public static final String FOREGROUND   = "foreground";
  public static final String BACKGROUND   = "background";
  public static final String TEXT_CASE    = "textCase";
  public static final String FONT         = "font";

  //--------------------------------------------------------------------
  // Instance variables
  // The instance variables for font, foreground and background color
  // are inherited from Canvas.
  protected String text = "default text";
  protected int topMargin = 4;
  protected int leftMargin = 4;
  protected int textCase = AS_IS;

  protected PropertyChangeSupport propertyListenerSupport;
  protected VetoableChangeSupport vetoListenerSupport;

  //--------------------------------------------------------------------
  // Constructor. All beans must have a no-argument constructor.
  public CaseAwareTextDisplay() {
    this.setSize(180, 30);
    propertyListenerSupport = new PropertyChangeSupport(this);
    vetoListenerSupport     = new VetoableChangeSupport(this);
  }
  //--------------------------------------------------------------------
  // Getters and setters. All properties (except font - which is inherited 
  // from Canvas) are bound. Foreground and background color are both bound
  // and constrained.
  public String getText() {
    return text;
  }
  public void setText(String text) {
    String oldValue = this.text;
    this.text = text;
    firePropertyChange(TEXT, oldValue, this.text);
  }
  public int getTopMargin() {
    return topMargin;
  }
  public void setTopMargin(int pixels) {
    int oldValue = this.topMargin;
    this.topMargin = pixels;
    firePropertyChange(TOP_MARGIN, new Integer(oldValue), 
                                           new Integer(this.topMargin));
  }
  public int getLeftMargin() {
    return leftMargin;
  }
  public void setLeftMargin(int pixels) {
    int oldValue = this.leftMargin;
    this.leftMargin = pixels;
    firePropertyChange(LEFT_MARGIN, new Integer(oldValue), 
                                            new Integer(this.leftMargin));
  }
  public void setForeground (Color newColor) {
    Color oldValue = getForeground();
    try {
      fireVetoableChange(FOREGROUND, oldValue, newColor);
      // Set new color only when change not vetoed.
      super.setForeground(newColor);
      // Inform bound beans of property change
      firePropertyChange(FOREGROUND, oldValue, newColor);
      repaint();
    }
    catch (PropertyVetoException e) {}
  }
  public void setBackground (Color newColor) {
    Color oldValue = getBackground();
    try {
      fireVetoableChange(BACKGROUND, oldValue, newColor);
      // Set new color only when change not vetoed.
      super.setBackground(newColor);
      // Inform bound beans of property change
      firePropertyChange(BACKGROUND, oldValue, newColor);
      repaint();
    }
    catch (PropertyVetoException e) {}
  }
  public int getTextCase() {
    return textCase;
  }
  public void setTextCase(int textCase) {
    int oldValue = this.textCase;
    this.textCase = textCase;
    firePropertyChange(TEXT_CASE, new Integer(oldValue), 
                                          new Integer(this.textCase));
    repaint();
  }
  //--------------------------------------------------------------------
  // Paint method overrides paint in Canvas.
  public void paint (Graphics g) {
    // Convert the text to the proper case
    String convertedText = getText();
    if (convertedText == null)
      convertedText = " ";
    switch (textCase) {
      case UPPERCASE:
        convertedText = convertedText.toUpperCase();
        break;
      case LOWERCASE:
        convertedText = convertedText.toLowerCase();
        break;
      case FIRST_IN_CAPS:
        convertedText = convertedText.toLowerCase();
        char [] temp = convertedText.toCharArray();
        char previous = ' ';
        for (int i = 0; i < temp.length; i ++)
        {
          if (previous == ' ')
            temp[i] = Character.toUpperCase(temp[i]);
          previous = temp[i];
        }
        convertedText = new String(temp);
        break;
      case AS_IS:
      default:
        // Text should be explicitly left as is, or the value for property
        // is unknown, so make no change to the case of the text.
    }
    // Paint the text
    Rectangle r = getBounds();
    Font font = getFont();
    if (font == null)
      font = new Font("Dialog", Font.PLAIN, 12);
    FontMetrics fm = Toolkit.getDefaultToolkit().getFontMetrics(font);

    int x = leftMargin;
    int y = topMargin + fm.getAscent();

    g.setColor(getBackground());
    g.clearRect(r.x, r.y, r.width, r.height);

    g.setColor(getForeground());
    g.setFont(getFont());
    g.drawString(convertedText, x, y);
  }
  //--------------------------------------------------------------------
  // Methods to support bound properties.
  public void addPropertyChangeListener(PropertyChangeListener listener) {
    propertyListenerSupport.addPropertyChangeListener(listener);
  }
  public void removePropertyChangeListener(PropertyChangeListener listener) {
    propertyListenerSupport.removePropertyChangeListener(listener);
  }
  protected void firePropertyChange(String propertyName,
                                    Object oldValue, Object newValue) {
   propertyListenerSupport.firePropertyChange(propertyName,oldValue,newValue);
  }
  //--------------------------------------------------------------------
  // Methods to support constrained properties.
  public void addVetoableChangeListener(VetoableChangeListener vetoListener) {
    vetoListenerSupport.addVetoableChangeListener(vetoListener);
  }
  public void removeVetoableChangeListener(VetoableChangeListener vetoListener) {
    vetoListenerSupport.removeVetoableChangeListener(vetoListener);
  }
  protected void fireVetoableChange(String propertyName, Object oldValue,
                            Object newValue) throws PropertyVetoException {
    vetoListenerSupport.fireVetoableChange(propertyName, oldValue, newValue);
  }
}


Listing Two

package mybeans;
import java.awt.Image;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.beans.SimpleBeanInfo;
// CaseAwareTextDisplayBeanInfo
public class CaseAwareTextDisplayBeanInfo extends SimpleBeanInfo
{
  public static final String BEAN_CLASS_NAME="mybeans.CaseAwareTextDisplay";
  //------------------------------------------------------------
  // Define an icon for the bean. Image must be in the bean's subdirectory
  // in the jar, but the subdirectory is not referred to here since its known 
  // from the package (fully qualified class name).
  public Image getIcon(int iconKind) {
    if (iconKind == BeanInfo.ICON_COLOR_32x32)
      return loadImage(getBeanIconString());
    else
      return null;
  }
  //------------------------------------------------------------
  // Return the property descriptors
  public PropertyDescriptor[] getPropertyDescriptors() {

    PropertyDescriptor[] pd = new PropertyDescriptor[6];
    PropertyDescriptor[] superPd;
    PropertyDescriptor[] finalPd;

    try {
      // Build the property descriptor for the text property
      pd[0] =
        new PropertyDescriptor( CaseAwareTextDisplay.TEXT,
                      Class.forName(BEAN_CLASS_NAME), "getText", "setText");
      pd[0].setBound(true);
      pd[0].setConstrained(false);
      pd[0].setDisplayName("text");
      pd[0].setExpert(false);
      pd[0].setHidden(false);

      pd[0].setShortDescription("The text to be displayed.");

      // Build the property descriptor for the topMargin property
      pd[1] =
        new PropertyDescriptor( CaseAwareTextDisplay.TOP_MARGIN,
             Class.forName(BEAN_CLASS_NAME),"getTopMargin", "setTopMargin");
      pd[1].setBound(true);
      pd[1].setConstrained(false);
      pd[1].setDisplayName("top margin");
      pd[1].setExpert(false);
      pd[1].setHidden(false);
      pd[1].setShortDescription("The top margin in pixels.");

      // Build the property descriptor for the leftMargin property
      pd[2] =
        new PropertyDescriptor( CaseAwareTextDisplay.LEFT_MARGIN,
           Class.forName(BEAN_CLASS_NAME), "getLeftMargin", "setLeftMargin");
      pd[2].setBound(true);
      pd[2].setConstrained(false);
      pd[2].setDisplayName("left margin");
      pd[2].setExpert(false);
      pd[2].setHidden(false);
      pd[2].setShortDescription("The left margin in pixels.");

      // Build the property descriptor for the foreground property
      pd[3] =
        new PropertyDescriptor( CaseAwareTextDisplay.FOREGROUND,
          Class.forName(BEAN_CLASS_NAME), "getForeground", "setForeground");
      pd[3].setBound(true);
      pd[3].setConstrained(true);
      pd[3].setDisplayName("foreground");
      pd[3].setExpert(false);
      pd[3].setHidden(false);
      pd[3].setShortDescription("The foreground color for the text.");

      // Build the property descriptor for the background property
      pd[4] =
        new PropertyDescriptor( CaseAwareTextDisplay.BACKGROUND,
          Class.forName(BEAN_CLASS_NAME), "getBackground", "setBackground");
      pd[4].setBound(true);
      pd[4].setConstrained(true);
      pd[4].setDisplayName("background");
      pd[4].setExpert(false);
      pd[4].setHidden(false);
      pd[4].setShortDescription("The background color for the text.");

      // Build the property descriptor for the textCase property
      pd[5] = getTextCasePropertyDescriptor();
    }
    catch (Throwable t) {
      t.printStackTrace();
      return null;      // use default design patterns
    }
    // Get the property descriptors of the superclass. The font property is 
    // implemented by Canvas. This logic is needed because some tools 
    // do not call getAdditionalBeanInfo.
    try {
    BeanInfo superBeanInfo = 
    Introspector.getBeanInfo(Class.forName(BEAN_CLASS_NAME).getSuperclass());
    superPd = superBeanInfo.getPropertyDescriptors();
    }
    catch (Throwable t) {
      t.printStackTrace();
      return null;      // use default design patterns
    }
    // Add the two sets of property descriptors to a single array
    finalPd = new PropertyDescriptor[superPd.length + pd.length];
    for (int i = 0; i < superPd.length; i++)
      finalPd[i] = superPd[i];
    for (int i = superPd.length; i < (superPd.length + pd.length); i++)
      finalPd[i] = pd[i - superPd.length];
    // Return the array of property descriptors
    return finalPd;
  }
  //-------------------------------------------------------------------------
  // Return the bean info for the superclasses, excluding the Object class.
  // This handles the font property which is implemented in the Canvas class.
  public BeanInfo[] getAdditionalBeanInfo() {
    try {
     BeanInfo[] bi = new BeanInfo[1];
     bi[0] = 
    Introspector.getBeanInfo(Class.forName(BEAN_CLASS_NAME).getSuperclass());
     return bi;
    }
    catch (Throwable t) {
    }
    return null;
  }
  //-------------------------------------------------------------------------
  // Returns a property descriptor for the textCase property. This is
  // implemented as a separate method solely for convenience in subclassing.
  protected PropertyDescriptor getTextCasePropertyDescriptor()
  throws IntrospectionException, ClassNotFoundException {
    PropertyDescriptor pd;
    pd = new PropertyDescriptor(CaseAwareTextDisplay.TEXT_CASE,
               Class.forName(BEAN_CLASS_NAME), "getTextCase", "setTextCase");
    pd.setBound(true);
    pd.setConstrained(false);
    pd.setDisplayName("case");
    pd.setExpert(false);
    pd.setHidden(false);
    pd.setShortDescription("Case to use for displaying the text.");
    return pd;
  }
  //-------------------------------------------------------------------------
  // Returns the name of the file containing the beans icon. This is
  // implemented as a separate method solely for convenience in subclassing.
  protected String getBeanIconString() {
    return "Earth.gif";
  }
}


Listing Three

Manifest-Version: 1.0

Name: mybeans/CaseAwareTextDisplay.class
Java-Bean: true 

Name: mybeans/CaseAwareTextDisplayC.class
Java-Bean: true 

Name: mybeans/CaseAwareTextDisplayL.class
Java-Bean: true 

Name: mybeans/CaseAwareTextDisplayT.class
Java-Bean: true 

Name: mybeans/ColorConstrainer.class
Java-Bean: true 



8


