HW5 Solutions Page

Return to homework page.

Here are the GIF files I used for this HW. I made them myself and as it wasn't the important part of the HW they are somewhat crude. The GIF comment fields on all of them are set so they can be used by my program to set the button's tool tips.

head.gif eraser.gif part0.gif part1.gif part2.gif part3.gif part4.gif part5.gif part6.gif part7.gif part8.gif

// cs151sec3hw5file1.java - 1st iteration of Potato Head application
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;

/**
   This is the first iteration of our potato head application.
    In this iteration we only have a potato HeadPanel with
    holes on it. When holes are clicked, they are replaced with
    the image of a body part appropriate for that region of
    a potato's body.
*/
public class cs151sec3hw5file1 extends JFrame
{
   /**
      Constructs our frame, reads in the potato image, and the shapes
      that can be drawn on it, set up Hole's on potato, and listener's
      associated with the HeadPanel.
   */  
   public cs151sec3hw5file1()
   {
       initializeParts();
       initializeHead();
       initializeListeners();
       setTitle(getClass().getName()); /* use reflection to get
                                          name of our frame right */
   }

   /*
     Reads in the shapes we can draw into ImageIcon's
   */
   protected void initializeParts()
   {
      parts = new ImageIcon[NUM_HOLES];

      for(int i = 0; i < NUM_HOLES; i++)
      {  
         parts[i] = new ImageIcon("part"+i+".gif");
      }
   }

   /*
     Sets up the HeadPanel of our potato head and puts it
     on our frame. Although we've hard-coded the locations
     of the Hole's on the potato head we're could change these
     by overriding this method in subclasses.
   */
   protected void initializeHead()
   {
      Container c = getContentPane();

      holes = new Hole[NUM_HOLES];
      
      holes[0] = new Hole(100, 20);
      holes[1] = new Hole(20, 90);
      holes[2] = new Hole(100, 60);
      holes[3] = new Hole(180, 90);
      holes[4] = new Hole(20, 150);
      holes[5] = new Hole(100, 130);
      holes[6] = new Hole(180, 150);
      holes[7] = new Hole(100, 180);
      holes[8] = new Hole(100, 220);

      hp = new HeadPanel("head.gif", holes);
      c.add(hp);
   }

   /*
     Sets up the listener on the HeadPanel which
     checks if clicked any Hole's.
   */
   protected void initializeListeners()
   {
       hp.addMouseListener(new MouseAdapter()
          {
              public void mouseClicked(MouseEvent e)
              {
                  cs151sec3hw5file1.this.mouseClicked(e);
              }
          }
       );
   }

   /*
     Called when a mouse is clicked over our HeadPanel.
     It checks if click was in any Hole on the panel
     and if so update the Hole.
   */
   protected void mouseClicked(MouseEvent e)
   {
      int x = e.getX();
      int y = e.getY();

      for(int i = 0 ; i < NUM_HOLES; i++)
      {
         if( holes[i].inside(x,y)) clickedHole(i);
      }
      
      repaint();       
   }

   /*
     Called when holes[i] has been clicked.
     It this iteration we just draw the body part usually
     associated with where the Hole is.
   */
   protected boolean clickedHole(int i)
   {
       holes[i].setImageIcon(parts[i]);
       return true;
   }

   /*
      creates our application frame and sets closing behavior.
   */
   public static void main(String[] args)
   {
       cs151sec3hw5file1 frame = new cs151sec3hw5file1();
       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
       frame.pack();
       frame.show();
   }

   public static int NUM_HOLES = 9; // number of holes on our potato

   protected HeadPanel hp; //panel where potato drawn
   protected ImageIcon[] parts; //different body part that we might draw 
   protected Hole[] holes; // holes we can put body parts into
}

//Hole.java - one hole on the potato

import java.awt.*;
import javax.swing.*;
import java.io.Serializable;

/**
   Class used to capture the notion of one hole on a Potato HEad
   figure. The Hole can be filled or not with a ImageIcon.
*/
public class Hole implements Serializable, Cloneable
  /* we want to write Hole's to disk and we want to be able to copy Hole's.
     so we implement these interfaces */
{
   /**
      create a blank hole at given coordinate
   */
   public Hole(int centerX, int centerY)
   {
      this(centerX, centerY, DEFAULT_WIDTH, DEFAULT_HEIGHT);
   }

   /**
   */
   public Hole(int centerX, int centerY, int width, int height)
   {
      this.centerX = centerX;
      this.centerY = centerY;
      this.width = width;
      this.height = height;
   }

   /**
      Used to produce a copy of the current Hole.
      Used to update the currentState in iterations 2 and 3.
      
      @return - the copy.
   */
   public Object clone()
   {
      Hole h = new Hole(centerX, centerY, width, height);
      h.setImageIcon(image);
      return h;
   }

   /**
      draws the current Hole to the given graphics context

      @param g - where we draw Hole to.
   */
   public void drawHole(Graphics g)
   {
      if(image == null)
      {
         g.setColor(Color.white);  
         g.fillOval(centerX - width/2, centerY - height/2, width, height);
      }
      else
        image.paintIcon(null, g, centerX - width/2, centerY - height/2);         
   }

   /**
     Sets the image that is in the hole. If null revert
     to drawing ovals.

      @param image - ImageIcon used to draw hole
   */
   public void setImageIcon(ImageIcon image)
   {
      this.image = image;
      if (image != null)
      {
         width = image.getIconWidth();
         height = image.getIconHeight();
      }
      else
      {
         width = DEFAULT_WIDTH;
         height = DEFAULT_HEIGHT;
      }
   }

   /**
      @return whether or not coordinate (x,y) in Hole area.
   */
   public boolean inside(int x, int y)
   {
      return (centerX - width/2 < x && x < centerX + width/2 &&
              centerY - height/2 < y && y < centerY + height/2);
   }

   int centerX; // x - coordinate of center of Hole
   int centerY; // y - coordinate of center of Hole
   ImageIcon image; // shape image to drawn in Hole
   int width; //width of shape image to be drawn in Hole
   int height; //height of shape image to be drawn in Hole
   
   /*
     when Hole has been filled with a shape image, use next to
     contants to draw an oval.
   */
   public static final int DEFAULT_WIDTH = 20;
   public static final int DEFAULT_HEIGHT = 20;
}

//HeadPanel.java -- panel for our potato

import java.awt.*;
import javax.swing.*;

/**
   Panel on which potato head will be drawn.
*/
public class HeadPanel extends JPanel
{
   /**
      construct a potato with a given potato head image
      and a given set of holes.

      @param head - name of gif file with image of potato head
      @param holes - array of holes used to draw shapes on potato
   */
   public HeadPanel(String head, Hole[] holes)
   {
      this.head = new ImageIcon(head);
      this.holes = holes;
   }

   /**
      Called when HeadPanel repainted to draw potato and any shapes on it

      @param g - the graphics context on to which the potato
                 will be drawn.
   */
   public void paintComponent(Graphics g)
   {
      head.paintIcon(null, g, 0, 0);

      for(int i = 0; i < holes.length; i++)
         holes[i].drawHole(g);
   }

   /**
      @return - the holes that are on the potato image.
   */
   public Hole[] getHoles()
   {
      return holes;
   }

   /**
      Sets the holes to be drawn on the potato head.

      @param holes - Hole's to be drawn on potato head
   */
   public void setHoles(Hole[] holes)
   {
       this.holes = holes;
   }

   /**
      @return - the size of the potato head
   */
   public Dimension getPreferredSize()
   {
      return new Dimension(head.getIconWidth(), head.getIconHeight());
   }
   
   ImageIcon head; // image of potato
   Hole[] holes; // holes used to draw images on the potato

}

// cs151sec3hw5file2.java - Iteration 2 of Potato Head Application
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;

/**
   This is the second iteration of our Potato Head application.
   In this iteration we add a buttonPanel which can be used
   to select body parts that we can put into the holes of the
   potato. We also have in the top-right a eraser tool. To
   remove a body part from the potato if we made a mistake.

   The different functionality of body parts versus eraser is
   handled via using the State Design Pattern.

   Some usability issues considered in this iteration were:
   (1) When we click on a button we set and keep it background
       red until next button clicked. This serves a clue to
       the user which tool the user is using.
   (2) We change the cursor to the current tool when it is over
       the HeadPanel as another clue to the user.
   (3) We set the tool tip for each of the buttons according
       to the GIF comment of the GIF on that button. This
       let's the user guess what each button is for.
   (4) Eraser is in top right corner and not mixed with body part.

*/
public class cs151sec3hw5file2 extends cs151sec3hw5file1
{

   /*
     Sets up the eraser tool button as well as the buttons
     to select a body part to put into a hole on the potato head.
     These are all set up on a panel which is then added to the
     current content pane.

     Notice by putting body parts in an array as we did and because
     our GIF files have names in a standard format it would be
     straightforward to add additional body parts.
   */ 
   protected void initializeParts()
   {
      super.initializeParts();
      curState = null;

      JPanel buttonPanel = new JPanel();
      buttonPanel.setLayout(new GridLayout(NUM_HOLES/2+1,2));

      Container c = getContentPane();
      c.setLayout(new FlowLayout());

      eraserButton = new JButton(new ImageIcon("eraser.gif"));
      eraserButton.setToolTipText("Eraser");
      buttonPanel.add(eraserButton);

      buttons = new JButton[NUM_HOLES];
      for(int i = 0; i < NUM_HOLES; i++)
      {
         buttons[i] = new JButton(parts[i]);
         buttons[i].setToolTipText(
           (String)parts[i].getImage().getProperty("comment", null));
         buttonPanel.add(buttons[i]);
      }
      
      c.add(buttonPanel);
   }

   /*
      Sets up the listeners associated with the eraserButton
      and the buttons to select a body part.
   */
   protected void initializeListeners()
   {
       super.initializeListeners();

      eraserButton.addActionListener(
         new ActionListener()
         {
             public void actionPerformed(ActionEvent e)
             {
                cs151sec3hw5file2.this.actionErasePerformed(e);
             }
         }

      );

      for(int i = 0; i < NUM_HOLES; i++)
      {
         buttons[i].addActionListener(
            new ActionListener()
            {
                public void actionPerformed(ActionEvent e)
                {
                     cs151sec3hw5file2.this.actionPartPerformed(e);
                }
            }
         );
      }      
   }

   /*
     Called erase button clicked. Method below
     sets the background of the button red and restores
     the previous button to the non-selected state.
     Cursor is set to be eraser cursor when over HeadPanel.
     currentState variable is updated to the appropriate
     EraseState.
   */
   protected void actionErasePerformed(ActionEvent e)
   {
       Hole hole = updateButtonsGetHole(eraserButton);
       int oldHoleIndex = (curState != null) ? 
          curState.getOldHoleIndex() : 0;
                
       curState = new EraseState((ImageIcon)eraserButton.getIcon(),
          eraserButton, hole, oldHoleIndex);
   }

   /*
     Called when one of the body part buttons clicked. Method below
     sets the background of the button red and restores
     the previous button to the non-selected state.
     Cursor is set to be body part image when over HeadPanel.
     currentState variable is updated to the appropriate
     DrawState.
   */
   protected void actionPartPerformed(ActionEvent e)
   {
       JButton button = (JButton)e.getSource();

       Hole hole = updateButtonsGetHole(button);
       int oldHoleIndex = (curState != null) ? 
          curState.getOldHoleIndex() : 0;
         
       curState = new DrawState((ImageIcon)button.getIcon(), 
          button, hole, oldHoleIndex);
      
   }

   /*
     Sets the background of the button passed to be
     red and returns any previously clicked button to
     their default value. Changes HeadPanel cursor to
     button's icon. Returns last clicked Hole.

     button - which we are changing.
   */
   protected Hole updateButtonsGetHole(JButton button)
   {
       Hole hole = null;
       JButton oldButton = null;
       ImageIcon icon = (ImageIcon)button.getIcon();

       button.setBackground(Color.red);

       // Set the cursor when over the HeadPanel to be button's icon.
       
       try
       {
          hp.setCursor(hp.getToolkit().createCustomCursor(
             icon.getImage(),
             new Point(0, 0),
             button.getToolTipText()));
       }
       catch(Exception exception) { exception.printStackTrace();}

       if(curState != null)
       { 
          hole = curState.getHole();
          oldButton = curState.getJButton();
          if(oldButton != null && oldButton != button) 
             oldButton.setBackground(Color.lightGray);
       }

       return hole;
   }

   /*
     Called when holes[i] has been clicked. (see cs151sec3hw5file1)
     In this iteration when a Hole is clicked we draw the ImageIcon
     returned by the getImageIcon() method of the currentState.
     This will be the last clicked body part if currentState is a 
     DrawState and will be null if it is an EraseState.

     So this method is part of the State pattern.

     i - index of Hole that has been clicked
   */
   protected boolean clickedHole(int i)
   {

      if(curState == null) return false;

      curState.setHole((Hole)holes[i].clone());
      curState.setOldHoleIndex(i);

      holes[i].setImageIcon(curState.getImageIcon());
      repaint();  
      return true;
   }

   /*
      Creates our application frame and sets closing behavior.
      Notice this method shadows cs151sec3hw5file1's method. 
   */
   public static void main(String[] args)
   {
       cs151sec3hw5file2 frame = new cs151sec3hw5file2();
       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
       frame.pack();
       frame.show();
   }

   protected State curState; /*
                                Keeps track of the last button
                                clicked, the last hole changed,
                                and what to draw or to erase.
                             */
   protected JButton[] buttons; // buttons to used to select draw tool
   protected JButton eraserButton; // button for erase draw tool   
}

//State.java - stores current state of Potato Head.

import java.awt.*;
import javax.swing.*; 

/**
   This abstract class is used as part of a State Design Pattern.
   It encapsulates the idea of a state of the Potato head application.
   It keeps track of last button clicked, last hole clicked, and its index. 
   getImageIcon() is called when we click on a hole and what it does
   is determined by subclasses. For what we've implemented it either
   erase whats in the Hole by returning null or returns the ImageIcon
   we're supposed to be drawing into Hole's.   
*/
public abstract class State
{
   /**
      Default constructor. Initializes everything to their null values
   */
   public State()
   {
      icon = null;
      button = null;
      hole = null;
      oldHoleIndex = 0;
   }

   /**
      construct a state with the given parameters

      @param icon - image we'll draw in this state
      @param button - last button clicked
      @param hole - last hole clicked
      @param oldHoleIndex - index of last hole clicked
   */
   public State(ImageIcon icon, JButton button, Hole hole, int oldHoleIndex)
   {
      this.icon = icon;
      this.button = button;
      this.hole = hole;
      this.oldHoleIndex = oldHoleIndex;

   }

   /**
      This method called when click on a Hole in the Potato Head.
      It is specified in subclasses and is part of the State
      Design Pattern. In our case, the EraseState subclass always
      returns null to erase the image in a Hole. The DrawState         
      class returns the ImageIcon stored in this class.

      @return either null or ImageIcon stored in State
   */
   public abstract ImageIcon getImageIcon();   

   /**
   */
   public JButton getJButton()
   {
      return button;
   }

   /**
      @return Hole stored in this state
   */
   public Hole getHole()
   {
      return hole;
   }

   /**
      @return index of last hole clicked
   */
   public int getOldHoleIndex()
   {
      return oldHoleIndex;
   }

   /**
      Used to set the value of the hole stored in this state.
      Should be the hole that was last clicked.

      @param hole - Hole to store.
      
   */
   public void setHole(Hole hole)
   {
      this.hole = hole;
   }

   /**
      stores value of the index of the Hole in the holes array of
      the Hole that was last clicked.

      @param oldHoleIndex - the index to store
   */
   public void setOldHoleIndex(int oldHoleIndex)
   {
      this.oldHoleIndex = oldHoleIndex;
   }


   int oldHoleIndex; // index of last hole clicked
   ImageIcon icon; // what will draw when click next hole clicked                      
   JButton button; // last clicked button
   Hole hole; // last hole clicked
 
}

//DrawState.java - state for drawing shapes on potato
import java.awt.*;
import javax.swing.*;

/**
   Subclass of State used for drawing body parts
   on the potato head.
*/
public class DrawState extends State
{
   /**
      Constructor to set up DrawState
      Just calls State's constructor

      @param icon - image we'll draw in this state
      @param button - last button clicked
      @param hole - last hole clicked
      @param oldHoleIndex - index of last hole clicked
   */
   public DrawState(ImageIcon icon, JButton button, Hole hole,
                    int oldHoleIndex)
   {
       super(icon, button, hole, oldHoleIndex);
   }

   /**
      @return the image to draw
   */
   public ImageIcon getImageIcon()
   {
      return icon;
   }   
   
}

// EraseState.java - state for erasing shapes off potato
import java.awt.*;
import javax.swing.*;

/**
   State in after eraser tool selected. The getImageIcon()
   method in this state always returns null so as to erase
   the contents of a hole
*/
public class EraseState extends State
{
   /**
      constructor for EraseState just calls State's constructor

      @param icon - image we'll draw in this state
      @param button - last button clicked
      @param hole - last hole clicked
      @param oldHoleIndex - index of last hole clicked
     
   */
   public EraseState(ImageIcon icon, JButton button, Hole hole, 
      int oldHoleIndex)
   {
       super(icon, button, hole, oldHoleIndex);
   }

   /**
      erase what's currently in a hole by setting it's image to
      null
   */
   public ImageIcon getImageIcon()
   {
      return null;
   }   
   
}

//cs151sec3hw5file1 - file used in our 3rd iteration
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.io.*;

/**
   Third iteration of our Potato Head draw program application.
   This iteration now has menus to allow opening and saving
   our creation. It also has an undo last operation feature and
   instruction and about information.

   Some comments on GUI design:

   (1) To keep people mental model simple undo undoes only one operation
   and doing undo twice undoes the undo. Opening a file sets
   the currentState to null.    

   (2) We added separators in our menus to separate distinct kinds of
   operations 
*/
public class cs151sec3hw5file3 extends cs151sec3hw5file2
{
   /**
      Besides call previous iterations constructors,
      set up menus and set the currentFile to be null
   */
   public cs151sec3hw5file3()
   {
       super();
       initializeMenus();
       currentFile = null;       
   }

   /*
     sets up the menu bar and its menus for our application.
   */
   protected void initializeMenus()
   {
      menuBar = new JMenuBar();
      setJMenuBar(menuBar);

      initializeFileMenu();
      initializeEditMenu();
      initializeHelpMenu();
   }

   /*
      sets up the file menu on the menu bar and its listeners
   */
   protected void initializeFileMenu()
   {
      fileMenu = new JMenu("File");
      menuBar.add(fileMenu);

      Action open = new AbstractAction("Open", null)
      {
         public void actionPerformed(ActionEvent e)
         {
            open();
         }
      };  
      fileMenu.add(open);
      fileMenu.addSeparator();

      Action save = new AbstractAction("Save", null)
      {
         public void actionPerformed(ActionEvent e)
         {
            save();
         }
      }; 
      fileMenu.add(save);

      Action saveAs = new AbstractAction("Save as", null)
      {
         public void actionPerformed(ActionEvent e)
         {
            saveAs();
         }
      };
      fileMenu.add(saveAs);
      fileMenu.addSeparator();
      
      Action exit = new AbstractAction("Exit", null)
      {
         public void actionPerformed(ActionEvent e)
         {
            System.exit(0);
         }
      };
      fileMenu.add(exit);
  
   }

   /*
     sets up the edit menu on the menu bar and its listeners
   */
   protected void initializeEditMenu()
   {
      editMenu = new JMenu("Edit");
      menuBar.add(editMenu);

      Action undo = new AbstractAction("Undo", null)
      {
         public void actionPerformed(ActionEvent e)
         {
            undo();
         }
      };  
      editMenu.add(undo);
   }

   /*
     sets up the help menu on the menu bar and its listeners.
   */
   protected void initializeHelpMenu()
   {
      helpMenu = new JMenu("Help");
      menuBar.add(helpMenu);

      Action instructions = new AbstractAction("Instructions", null)
      {
         public void actionPerformed(ActionEvent e)
         {
            JOptionPane.showMessageDialog(cs151sec3hw5file3.this, 
               instructionsString);
         }
      };  
      helpMenu.add(instructions);
      helpMenu.addSeparator();

      Action about = new AbstractAction("About", null)
      {
         public void actionPerformed(ActionEvent e)
         {
           JOptionPane.showMessageDialog(cs151sec3hw5file3.this, 
              aboutString);
         }
      };  
      helpMenu.add(about);

   }

   /*
     called by Open menu item's listener to actual open and
     read in a file
   */
   protected void open()
   {
      SelectFile(OPEN);
      try
      {
          ObjectInputStream in = new ObjectInputStream(
            new FileInputStream(currentFile));

          holes=(Hole[])in.readObject();

          hp.setHoles(holes);

          if(curState != null)
          {
             JButton button = curState.getJButton();
             if(button != null) button.setBackground(Color.lightGray);
          }

          curState = null;
          in.close();
      }
      catch(Exception ie)
      {
         ie.printStackTrace();
      }

      repaint();      
   }

   /*
     called by Save menu item's listener to actual save a
     file. Does a save as filename if no filename has been
     previously selected
   */
   protected void save()
   {
      if(currentFile == null) saveAs();
      
      try
      {
          ObjectOutputStream out = new ObjectOutputStream(
            new FileOutputStream(currentFile));

          out.writeObject(holes);
          out.close();
      }
      catch(IOException ie)
      {
         ie.printStackTrace();
      }
   }

   /*
     Called by Save As menu item listener.
     Saves a file with the name chosen using a JFileChooser
   */
   protected void saveAs()
   {
      SelectFile(SAVE);
      if(currentFile != null) save();
   }

   /*
     called by open and save operation.
     Uses a JFileChooser to set the currentFile File
     Object. 
   */
   protected  void SelectFile(int openSave) 
   {
      JFileChooser choose = new JFileChooser();
         choose. setFileSelectionMode( JFileChooser.FILES_ONLY);
        
      int result;

      if( openSave == SAVE)
         result = choose.showSaveDialog(this);
      else
         result = choose.showOpenDialog(this);

      if(result == JFileChooser.CANCEL_OPTION)
            return;
      currentFile = choose.getSelectedFile();

      if(currentFile == null) JOptionPane.showMessageDialog( 
         cs151sec3hw5file3.this,
         "Bad File Name", "Bad File Name",
         JOptionPane.ERROR_MESSAGE );
   }

   /*
     called when undo menu item selected to undo the
     last action we did on the potato
   */
   protected void undo()
   {
       if(curState == null) return;
       Hole tmp = curState.getHole();
       curState.setHole(holes[curState.getOldHoleIndex()]);
       holes[curState.getOldHoleIndex()] = tmp;
       repaint();
   }


   /*
      Creates our application frame and sets closing behavior.
      Notice this method shadows cs151sec3hw5file1's method. 
   */
   public static void main(String[] args)
   {
       cs151sec3hw5file3 frame = new cs151sec3hw5file3();
       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
       frame.pack();
       frame.show();
   }

   protected File currentFile; // file we'll read or write to
   protected JMenuBar menuBar; // programs menubar
   protected JMenu fileMenu; //the three menus off our menubar
   protected JMenu editMenu;
   protected JMenu helpMenu; 
   protected final static int OPEN = 1; /* used to select open
                                           mode for our JFileChooser
                                           code
                                        */
   protected final static int SAVE = 0; /* used to select save
                                           mode for our JFileChooser
                                           code
                                       */
   protected static String instructionsString = 
      "Mr. Potato Game\n" +
      "===============\n" +
      "To play click on a button to make it the\n" +
      "current drawing tool. Then click on a hole\n"+
      "on the potato. This will place that image in\n"+
      "the hole. Try to make a cool looking face!\n\n"+
      "The top left tool is an eraser and can be used\n"+
      "to take an image off a hole.\n\n" +
      "The menu bar can be used to save and open\n"+
      "your creations. You can also undo the last image\n"+
      "placed."; /* printed when instructions menu item selected 
                 */
   protected static String aboutString =
      "This program was written by Chris Pollett\n"+
      "in 2001 at SJSU.";
          // printed when about item selected from menu
}