import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import java.lang.reflect.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;

/**
   A component filled with editors for all editable properties 
   of an object.
*/
public class PropertySheet extends JPanel
{
   /**
      Constructs a property sheet that shows the editable
      properties of a given object.
      @param object the object whose properties are being edited
   */
   public PropertySheet(Object bean)
   {
      try
      {
         BeanInfo info 
            = Introspector.getBeanInfo(bean.getClass());
         PropertyDescriptor[] descriptors 
            = info.getPropertyDescriptors();      
         setLayout(new FormLayout());
         for (int i = 0; i < descriptors.length; i++)
         {
            PropertyEditor editor 
               = getEditor(bean, descriptors[i]);
            if (editor != null)
            {
               add(new JLabel(descriptors[i].getName()));
               add(getEditorComponent(editor));
            }
         }
      }
      catch (IntrospectionException exception)
      {
         exception.printStackTrace();
      }
   }

   /**
      Gets the property editor for a given property,
      and wires it so that it updates the given object.
      @param bean the object whose properties are being edited
      @param descriptor the descriptor of the property to
      be edited
      @return a property editor that edits the property
      with the given descriptor and updates the given object
   */
   public PropertyEditor getEditor(final Object bean,
      PropertyDescriptor descriptor)
   {
      try
      {
         Method getter = descriptor.getReadMethod();
         if (getter == null) return null;
         final Method setter = descriptor.getWriteMethod();
         if (setter == null) return null;
         final PropertyEditor editor;
         Class editorClass = descriptor.getPropertyEditorClass();
         if (editorClass != null)            
            editor = (PropertyEditor) editorClass.newInstance();
         else
            editor = PropertyEditorManager.findEditor(
               descriptor.getPropertyType());
         if (editor == null) return null;

         Object value = getter.invoke(bean, new Object[] {});
         editor.setValue(value);
         editor.addPropertyChangeListener(new
            PropertyChangeListener()
            {
               public void propertyChange(PropertyChangeEvent event)
               {
                  try
                  {
                     setter.invoke(bean, 
                        new Object[] { editor.getValue() });
                     fireStateChanged(null);
                  }
                  catch (IllegalAccessException exception)
                  {
                  }
                  catch (InvocationTargetException exception)
                  {
                  }
               }
            });
         return editor;
      }
      catch (InstantiationException exception)
      {
         return null;
      }
      catch (IllegalAccessException exception)
      {
         return null;
      }
      catch (InvocationTargetException exception)
      {
         return null;
      }
   }

   /**
      Wraps a property editor into a component.
      @param editor the editor to wrap
      @return a button (if there is a custom editor), 
      combo box (if the editor has tags), or text field (otherwise)
   */      
   public Component getEditorComponent(final PropertyEditor editor)   
   {      
      String[] tags = editor.getTags();
      String text = editor.getAsText();
      if (editor.supportsCustomEditor())
      {
         // Make a button that pops up the custom editor
         final JButton button = new JButton();
         // if the editor is paintable, have it paint an icon
         if (editor.isPaintable())
         {
            button.setIcon(new 
               Icon()
               {
                  public int getIconWidth() { return WIDTH - 8; }
                  public int getIconHeight() { return HEIGHT - 8; }

                  public void paintIcon(Component c, Graphics g, 
                     int x, int y)
                  {
                     g.translate(x, y);
                     Rectangle r = new Rectangle(0, 0, 
                        getIconWidth(), getIconHeight());
                     Color oldColor = g.getColor();
                     g.setColor(Color.BLACK);
                     editor.paintValue(g, r);
                     g.setColor(oldColor);
                     g.translate(-x, -y);
                  }
               });
         } 
         else 
            button.setText(buttonText(text));
         // pop up custom editor when button is clicked
         button.addActionListener(new
            ActionListener()
            {
               public void actionPerformed(ActionEvent event)
               {
                  JOptionPane.showMessageDialog(null,
                     editor.getCustomEditor());
                  if (editor.isPaintable())
                     button.repaint();
                  else 
                     button.setText(buttonText(editor.getAsText()));
               }
            });
         return button; 
      }
      else if (tags != null)
      {
         // make a combo box that shows all tags
         final JComboBox comboBox = new JComboBox(tags);
         comboBox.setSelectedItem(text);
         comboBox.addItemListener(new
            ItemListener()
            {
               public void itemStateChanged(ItemEvent event)
               {
                  if (event.getStateChange() == ItemEvent.SELECTED)
                     editor.setAsText(
                        (String) comboBox.getSelectedItem());
               }
            });
         return comboBox;
      }
      else 
      {
         final JTextField textField = new JTextField(text, 10);
         textField.getDocument().addDocumentListener(new
            DocumentListener()
            {
               public void insertUpdate(DocumentEvent e) 
               {
                  try
                  {
                     editor.setAsText(textField.getText());
                  }
                  catch (IllegalArgumentException exception)
                  {
                  }
               }
               public void removeUpdate(DocumentEvent e) 
               {
                  try
                  {
                     editor.setAsText(textField.getText());
                  }
                  catch (IllegalArgumentException exception)
                  {
                  }
               }
               public void changedUpdate(DocumentEvent e) 
               {
               }
            });
         return textField;
      }
   }

   /**
      Formats text for the button that pops up a
      custom editor.
      @param text the property value as text
      @return the text to put on the button
   */
   private static String buttonText(String text)
   {
      if (text == null || text.equals("")) 
         return " ";
      if (text.length() > MAX_TEXT_LENGTH)
         return text.substring(0, MAX_TEXT_LENGTH) + "...";
      return text;
   }

   /**
      Adds a change listener to the list of listeners.
      @param listener the listener to add
   */
   public void addChangeListener(ChangeListener listener)
   {
      changeListeners.add(listener);
   }

   /**
      Notifies all listeners of a state change.
      @param event the event to propagate
   */
   private void fireStateChanged(ChangeEvent event)
   {
      for (int i = 0; i < changeListeners.size(); i++)
      {
         ChangeListener listener = (ChangeListener) changeListeners.get(i);
         listener.stateChanged(event);
      }
   }

   private ArrayList changeListeners = new ArrayList();
   private static final int WIDTH = 100;
   private static final int HEIGHT = 25;
   private static final int MAX_TEXT_LENGTH = 15;
}

