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