JMU
Advanced Uses of the Observer Pattern in Java
Multicasting, Property Changes, and Other Issues


Prof. David Bernstein
James Madison University

Computer Science Department
bernstdh@jmu.edu


A Common Use of the Oberver Pattern in Java GUIs
Creating Subjects of AWT Events
Creating Subjects of AWT Events (cont.)
Creating Subjects of AWT Events (cont.)
javaexamples/multicaster/CombinationField.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/**
 * A Component that can be used to enter secret combinations
 * using the keyboard. A combination can consist of any number
 * of characters. The combination is never displayed.
 * Users indicates that they are done by pressing [Enter].
 *   
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public class CombinationField extends JPanel implements KeyListener
{
  private static final long serialVersionUID = 1L;

  private ActionListener actionListener = null;
  Color   background;
  private int            id = ActionEvent.ACTION_FIRST;
  private JLabel         label;
  private String         combination;
  
  /**
   * Default Constructor.
   */
  public CombinationField()
  {
    super();

    setLayout(new BorderLayout());
    label = new JLabel("", SwingConstants.CENTER);
    add(label, BorderLayout.CENTER);
    background = getBackground();

    reset();
    
    setFocusable(true);
    requestFocusInWindow();
 }

  /**
   * Add an ActionListener to this Component. The ActionListener
   * will be informed when the user presses [Enter].
   * 
   * @param l  The ActionListener to add
   */
  public synchronized void addActionListener(ActionListener l) 
  {
    actionListener = AWTEventMulticaster.add(actionListener, l);
  }

  /**
   * Fire an ActionEvent to all listeners.
   */
  private void fireEvent()
  {
    ActionEvent ae = new ActionEvent(this, id, "CombinationEntered");
    id++;
    actionListener.actionPerformed(ae);
  }
  
  /**
   * Get the most recent combination that was entered
   * since the last call to reset().
   * 
   * @return  The combination
   */
  public String getCombination()
  {
    String result = combination;
    combination = "";
    return result;
  }

  /**
   * Handle keyTyped messages.
   * 
   * @param e The event that generated the message
   */
  public void keyTyped(KeyEvent e)
  {
  }

  /**
   * Handle keyPressed messages.
   * 
   * @param e The event that generated the message
   */
  public void keyPressed(KeyEvent e)
  {
    setBackground(Color.YELLOW);
    repaint();
    Toolkit.getDefaultToolkit().beep();
  }

  /**
   * Handle keyReleased messages.
   * 
   * @param e The event that generated the message
   */
  public void keyReleased(KeyEvent e)
  {
    setBackground(background);
    int code = e.getKeyCode();

    if (code == KeyEvent.VK_ENTER)
    {
      removeKeyListener(this);
      setText("Disabled");
      if (actionListener != null) fireEvent();
    }
    else
    {
      combination += e.getKeyChar();
    }
  }

  /**
   * Remove an ActionListener from this Component.
   * 
   * @param l  The ActionListener to remove
   */
  public synchronized void removeActionListener(ActionListener l) 
  {
    actionListener = AWTEventMulticaster.remove(actionListener, l);
  }

  /**
   * Reset the state of this Component.
   */
  public void reset()
  {
    combination = "";
    addKeyListener(this);
    label.setText("Enter A Combination");
  }
  
  /**
   * Set the text on this Component.
   * 
   * @param text  The text
   */
  public void setText(String text)
  {
    label.setText(text);
  }
}
        
Creating Subjects of Changes in Object State
Creating Subjects of Changes in Object State (cont.)
Creating Subjects of Changes in Object State (cont.)
A Subject
javaexamples/propertychange/Rectangle.java
import java.awt.*;
import java.beans.*;

/*
 * An encapsulation of a rectangle.
 * 
 * This implementation differs in significant ways from both the Rectangle class
 * and the Rectangle2D inner classes. Perhaps most importantly, it's
 * attributes are private so that it can serve as a property change subject.
 * 
 * @author  Prof. David Bernstein, James Madison Univerity
 * @version 1.0
 */
public class Rectangle
{
  private static final int  SQUARE_WIDTH = 3;  
  private static final int  SQUARE_GAP   = 1;  
  
  private boolean selected;
  private int height, width, x, y;
  private PropertyChangeSupport support;
  
  /**
   * Explicit value constructor.
   * 
   * @param x       The x-coordinate of the upper-left corner
   * @param y       The y-coordinate of the upper-left corner
   * @param width   The width
   * @param height  The height
   */
  public Rectangle(int x, int y, int width, int height)
  {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    selected = false;
    
    support = new PropertyChangeSupport(this);
  }
  
  /**
   * Add a PropertyChangeListener to this Rectangle.
   * The given object will actually listen to four different properties:
   * x, y, width, and height.
   * 
   * @param listener  The listener to add
   */
  public void addPropertyChangeListener(PropertyChangeListener listener)
  {
    support.addPropertyChangeListener("x", listener);
    support.addPropertyChangeListener("y", listener);
    support.addPropertyChangeListener("width", listener);
    support.addPropertyChangeListener("height", listener);
  }
  
  /**
   * Create a square visual grabber (for dragging a corner of this Rectangle).
   * 
   * @param x       The x-coordinate of the point of interest
   * @param y       The y-coordinate of the point of interest
   * @param deltaX  The horizontal offset (-1 for West, 1 for East)
   * @param deltaY  The vertical offset (-1 for North, 1 for South)
   * @return
   */
  private int[] createGrabber(int x, int y, int deltaX, int deltaY)
  {
    int x1 = x + deltaX * SQUARE_GAP;
    int y1 = y + deltaY * SQUARE_GAP;
    int x2 = x1 + deltaX * SQUARE_WIDTH;
    int y2 = y1 + deltaY * SQUARE_WIDTH;
    
    int sx = Math.min(x1, x2);
    int sy = Math.min(y1, y2);
    
    int[] result = {sx, sy, SQUARE_WIDTH, SQUARE_WIDTH};
    
    return result;
  }

  /**
   * Return true if this Rectangle contains the given Point.
   * 
   * @param p  The Point of interest
   * @return   true if p is contained in this Rectangle; false otherwise
   */
  public boolean contains(Point p)
  {
    return (p.x >= x) && (p.x <= x + width) && (p.y >= y) && (p.y <= y + height);
  }
  
  /**
   * Get the four corners for this Rectangle.
   * 
   * @return  The four corners;
   */
  public Point[] corners()
  {
    Point[] result = new Point[4];
    result[0] = new Point(x, y);                  // Upper-left
    result[1] = new Point(x + width, y);          // Upper-right
    result[2] = new Point(x + width, y + height); // Lower-left
    result[3] = new Point(x, y + height);         // Lower-left
    
    return result;
  }
  
  /**
   * Draw a rectangular area.
   * 
   * @param g  The rendering engine to use
   * @param r  The x, y, width, and height of the rectangular area
   */
  private void drawRect(Graphics g, int...r )
  {
    g.drawRect(r[0], r[1], r[2], r[3]);
  }
  
  /**
   * Fill a rectangular area.
   * 
   * @param g  The rendering engine to use
   * @param r  The x, y, width, and height of the rectangular area
   */
  private void fillRect(Graphics g, int...r )
  {
    g.fillRect(r[0], r[1], r[2], r[3]);
  }
  
  /**
   * Get the height.
   * 
   * @return  The height
   */
  public int getHeight()
  {
    return height;
  }
  
  /**
   * Get the width.
   * 
   * @return  The width
   */
  public int getWidth()
  {
    return width;
  }
  
  /**
   * Get the x-coordinate of the upper-left corner.
   * 
   * @return  The x-coordinate
   */
  public int getX()
  {
    return x;
  }
  
  /**
   * Get the y-coordinate of the upper-left corner.
   * 
   * @return  The x-coordinate
   */
  public int getY()
  {
    return y;
  }

  /**
   * Return true if this Rectangle is currently selected.
   * 
   * @return true if selected; false otherwise
   */
  public boolean isSelected()
  {
    return selected;
  }

  /**
   * Render this Rectangle.
   * 
   * @param g  The rendering engine to use
   */
  public void paint(Graphics g)
  {
    drawRect(g, x,  y,  width,  height);
    
    if (selected)
    {
      Color old = g.getColor();
      g.setColor(Color.BLUE);
      
      paintGrabbers(g, x, y);
      paintGrabbers(g, x+width, y);
      paintGrabbers(g, x+width, y+height);
      paintGrabbers(g, x, y+height);
      
      g.setColor(old);
    }
  }
  
  /**
   * Render the visual grabbers for a particular point/corner.
   * 
   * @param g  The rendering engine to use
   * @param x  The x-coordinate of the point/corner
   * @param y  The y-coordinate of the point/corner
   */
  private void paintGrabbers(Graphics g, int x, int y)
  {
    fillRect(g, createGrabber(x, y, -1, -1)); // NORTH_WEST grabber
    fillRect(g, createGrabber(x, y, -1,  1)); // NORTH_EAST grabber
    fillRect(g, createGrabber(x, y,  1,  1)); // SOUTH_EAST grabber
    fillRect(g, createGrabber(x, y,  1, -1)); // SOUTH_WEST grabber
  }

  /**
   * Remove a PropertyChangeListener (for all properties).
   * 
   * @param listener  The listener to remove.
   */
  public void removePropertyChangeListener(PropertyChangeListener listener)
  {
    support.removePropertyChangeListener(listener);
  }

  /**
   * Select this Rectangle or not.
   * 
   * @param selected  true to select; false to unselect
   */
  public void setSelected(boolean selected)
  {
    this.selected = selected;
  }
  
  /**
   * Set the height.
   * 
   * @param height The height
   */
  public void setHeight(int height)
  {
    support.firePropertyChange("height", this.height, height);
    this.height = height;
  }
  
  /**
   * Set the width.
   * 
   * @param width The width
   */
  public void setWidth(int width)
  {
    support.firePropertyChange("width", this.width, width);
    this.width = width;
  }
  
  /**
   * Set the x-coordinate of the upper-left corner.
   * 
   * @param x The x-coordinate
   */
  public void setX(int x)
  {
    support.firePropertyChange("x", this.x, x);
    this.x = x;
  }
  
  /**
   * Set the y-coordinate of the upper-left corner.
   * 
   * @param y The y-coordinate
   */
  public void setY(int y)
  {
    support.firePropertyChange("y", this.y, y);
    this.y = y;
  }
}
        
Creating Subjects of Changes in Object State (cont.)
An Observer
javaexamples/propertychange/PropertyLabel.java
import java.beans.*;
import javax.swing.*;

/**
 * A PropertyLabel is a JLabel that responds to appropriate
 * propertyChange messages by changing its text.
 * 
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public class PropertyLabel extends JLabel implements PropertyChangeListener
{
  private static final long serialVersionUID = 1L;

  private String property;

  /**
   * Explicit value constructor.
   * 
   * @param property  The property being displayed
   * @param value     The initial value to display
   */
  public PropertyLabel(String property, Object value)
  {
    super(value.toString());
    this.property = property;
    setBorder(BorderFactory.createTitledBorder(property));
  }

  /**
   * Handle propertyChange messages.
   * 
   * @param evt  The event that generated the message
   */
  public void propertyChange(PropertyChangeEvent evt)
  {
    if (property.equals(evt.getPropertyName()))
    {
      String text = evt.getNewValue().toString();
      setText(text);
    }
  }
}
        
Creating Subjects of Changes in Object State (cont.)
Usage
javaexamples/propertychange/RectanglePanel.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/**
 * A RectanglePanel can be used to display and edit a Rectangle.
 * 
 * @author  Prof. David Bernstein, James Madison University
 *
 */
public class RectanglePanel extends JPanel 
    implements MouseListener, MouseMotionListener
{
  private static final long serialVersionUID = 1L;

  private int               draggedCorner;
  private Rectangle         rect;
  
  /**
   * Explicit value constructor.
   * 
   * @param rect  The Rectangle to display/edit
   */
  public RectanglePanel(Rectangle rect)
  {
    this.rect = rect;
    addMouseListener(this);
    addMouseMotionListener(this);
  }

  /**
   * Handle mouseClicked messages.
   * 
   * @param e The event that generated the message
   */
  public void mouseClicked(MouseEvent e)
  {
    Point p = e.getPoint();

    if (!rect.isSelected())
    {
      if   (rect.contains(p)) rect.setSelected(true);
      else rect.setSelected(false);

      repaint();
    }
  }

  /**
   * Handle mouseDragged messages.
   * 
   * @param e The event that generated the message
   */
  public void mouseDragged(MouseEvent e)
  {
    if (rect.isSelected())
    {
      Point finish = e.getPoint();

      int xMin = rect.getX();
      int yMin = rect.getY();
      int xMax = xMin + rect.getWidth();
      int yMax = yMin + rect.getHeight();
      
      if (draggedCorner == 0)
      {
        xMin = finish.x;
        yMin = finish.y;
      }
      else if (draggedCorner == 1)
      {
        xMax = finish.x;
        yMin = finish.y;
      }
      else if (draggedCorner == 2)
      {
        xMax = finish.x;
        yMax = finish.y;
      }
      else if (draggedCorner == 3)
      {
        xMin = finish.x;
        yMax = finish.y;
      }
      
      rect.setX(Math.min(xMin, xMax));
      rect.setY(Math.min(yMin, yMax));
      rect.setWidth(Math.abs(xMax - xMin));
      rect.setHeight(Math.abs(yMax - yMin));
      
      repaint();
    }
  }

  /**
   * Handle mouseEntered messages.
   * 
   * @param e The event that generated the message
   */
  public void mouseEntered(MouseEvent e)
  {
  }

  /**
   * Handle mouseExited messages.
   * 
   * @param e The event that generated the message
   */
  public void mouseExited(MouseEvent e)
  {
  }

  /**
   * Handle mouseMoved messages.
   * 
   * @param e The event that generated the message
   */
  public void mouseMoved(MouseEvent e)
  {
  }

  /**
   * Handle mousePressed messages.
   * 
   * @param e The event that generated the message
   */
  public void mousePressed(MouseEvent e)
  {
    if (rect.isSelected())
    {
      Point p = e.getPoint();
      Point[] corners = rect.corners();
      double min = Double.POSITIVE_INFINITY;
      for (int i=0; i<corners.length; i++)
      {
        double d = p.distance(corners[i]);
        if (d < min)
        {
          min = d;
          draggedCorner = i;
        }
      }
    }
  }

  /**
   * Handle mouseReleased messages.
   * 
   * @param e The event that generated the message
   */
  public void mouseReleased(MouseEvent e)
  {
    if (rect.isSelected())
    {
      rect.setSelected(false);
      repaint();
    }
  }

  /**
   * Render this component.
   * 
   * @param g  The rendering engine to use
   */
  public void paint(Graphics g)
  {
    super.paint(g);
    rect.paint(g);
  }
}
        
What About Observer and Observable?