CPSC 225, Spring 2016
Lab 9: ECard Files

In Lab 7, you worked on a program that lets the user design simple "eCards", but there was no way for the user to save their work. For this lab, you will improve the program by allowing the user to save their work and to open a saved file for further editing.

You can use your solution to Lab 7 as the starting point for this lab. However, if you did not have a complete solution or were not completely satisfied with your own solution, you can use my program as your starting point instead. The file that you need for that is /classes/cs225/ECardPanelLab9.java.

If you worked with a partner on Lab 7, you can continue to work together. However, if you do that, you should add to the program at least two of the "enhancements" mentioned at the end of the lab. You are not required to continue to work with your partner.

In any case, you should begin by making a project named Lab9, and you should copy both the eCard program and JMenuRadioGroup.java into the src folder in that project.

This lab is due next week. It will be collected on Friday afternoon, November 4, at 3:00.

About File Dialogs

As covered in Section 11.2.3, The JFileChooser class can be used to show the usual "Open" and "Save" dialog boxes to the user. The methods for opening and saving a text file are pretty standard, except for the part where you actually read or write the selected file. You can copy-and-paste the methods from this page, and then you just have to complete them. You will need an instance variable, fileDialog, of type JFileChooser, for use by the methods. You will also have to import some classes.

public void doOpen() {
   if (fileDialog == null)
      fileDialog = new JFileChooser();
   fileDialog.setDialogTitle("Select File to be Opened");
   fileDialog.setSelectedFile(null);  // No file is initially selected.
   int option = fileDialog.showOpenDialog(this);
   if (option != JFileChooser.APPROVE_OPTION)
      return;  // User canceled or clicked the dialog's close box.
   File selectedFile = fileDialog.getSelectedFile();
   Scanner in;  // a scanner for reading text from the file
   try {
      in = new Scanner( selectedFile );
   }
   catch (Exception e) {
      JOptionPane.showMessageDialog(this,
            "Sorry, an error occurred while trying to open the file:\n" + e);
      return;
   }
   try {
      // ...
      // ...   (Read data from the file, using the scanner.)
      // ...
   }
   catch (Exception e) {
      JOptionPane.showMessageDialog(this,
            "Sorry, an error occurred while trying to read from the file:\n" + e);
   }
   finally {
      in.close();
   }
}


private void doSave() {
   if (fileDialog == null)      
      fileDialog = new JFileChooser(); 
   fileDialog.setSelectedFile(null); // Initially no file is selected. 
   File selectedFile;  //Initially selected file name in the dialog.
   fileDialog.setDialogTitle("Select File to be Saved");
   int option = fileDialog.showSaveDialog(this);
   if (option != JFileChooser.APPROVE_OPTION)
      return;  // User canceled or clicked the dialog's close box.
   selectedFile = fileDialog.getSelectedFile();
   if (selectedFile.exists()) {  // Ask the user whether to replace the file.
      int response = JOptionPane.showConfirmDialog( this,
            "The file \"" + selectedFile.getName()
            + "\" already exists.\nDo you want to replace it?", 
            "Confirm Save",
            JOptionPane.YES_NO_OPTION, 
            JOptionPane.WARNING_MESSAGE );
      if (response != JOptionPane.YES_OPTION)
         return;  // User does not want to replace the file.
   }
   PrintWriter out;  // For writing data to the file.
   try {
      out = new PrintWriter( selectedFile );
   }
   catch (Exception e) {
      JOptionPane.showMessageDialog(this,
         "Sorry, an error occurred while trying to open the file:\n" + e);
      return;
   }
   try {
       // ...
       // ... (Write data to the file, using the printwriter.)
       // ...
       out.flush();  // Make sure data is actually sent to the file!
       out.close();
       if (out.checkError())
          throw new Exception("Error in PrintWriter");
   }
   catch (Exception e) {
      JOptionPane.showMessageDialog(this,
         "Sorry, an error occurred while trying to write to the file:\n" + e);
   }
   finally {
      out.close();
   }
}

Saving and Opening ECard Files

The improved program will need a "File" menu. You should add a file menu containing the commands "New", "Open...", and "Save...". (A "..." added to a menu command usually means that the command will open a dialog box.) You can also add a "Quit" command if you want, which can be implemented simply by calling System.exit(0);

The "New" command can be implemented by clearing out the ArrayList that holds the list of items and setting some other data back to the default. The backgroundPaint should be reset to Color.WHITE). Note that to set the background overlay back to its default, you need to select or deselect items in the "Overlay" menu. A JCheckboxMenuItem has a method item.setSelected(trueOrFalse) to set whether it is checked or not, and a JMenuRadioGroup has the method group.setSelectedIndex(i) to select the ith item in the group.


But the main work of the lab is to implement "Open..." and "Save...". We have begun to look at Java's support for files, as covered in Chapter 11. The main classes for working with text files in a GUI program are Scanner (for reading from files), PrintWriter (for writing to files), and JFileChooser (for displaying the dialog box where the user can select files). You will need those three classes in your program.

To implement "Save...", you must somehow represent the state of the program as text, and write that text to the file. You need to design the file format that you will use. For example, to save the ArrayList of items, you can output the number of items followed by a line of text for each item. An item can be represented as a word specifying the type of item ("text" or "image") followed by the data that you need to recreate the item exactly as it was when it was saved.

One issue that you will run into is that an item in the ArrayList can be either a TextItem or an ImageItem. You can find out which by testing, for example: if (item instanceof TextItem)

You also need to save information about the state of the background and overlay. For the overlay, the information you need is just the selected index in the overlay color radio group and whether or not the two checkboxes are checked. For the background, there is the problem that your program does not keep track of what the current background is. You will probably need to add an instance variable (or several instance variables) to the program to keep track of the current background. The value of that variable will change whenever the user selects a new background from the "Background" menu.

Once you have implemented the "Save" command, use it to save a file and inspect the file to make sure that it is what you want. Then, you can move on to reading a file back into the program.

(A couple of notes: For a font, the methods font.getName(), font.getStyle(), and font.getSize() return the name, style, and size that you need for the font constructor. And a color has methods color.getRed(), color.getGreen(), and color.getBlue() to return the color components that are used in the Color constructor. However, you might have easier ways to save information about the specific fonts and colors that you are using in your program.)


To implement the "Open" command, you need to be able to read a file that was written by the "Save" command and restore the program to the state that was saved. Remember that errors can occur while reading a file, especially if the file that the user selects is not one that was written by your program. You really should not change the state of the program until you have successfully read all the data from the file. As you read the file, store the new state in local variables. When you have all the data, you can copy the data from the local variables into the instance variables of the program (and call repaint()).

(One note: Depending on how you wrote your ImageItem and TextItem classes, you might or might not be able to use the existing constructors in those classes to create the new items as you read the file. Remember that you can always add additional constructors to a class!)

Enhancements

There are many ways to enhance the program, to make it more like a normal GUI program. You might want to add some of these enhancements — and if you are working with a partner, you are required to do so...


Delete unwanted items. When an item is added by mistake, or when you just don't like it, it's nice to be able to remove it. You could add a "Remove Top Item" command to make that possible. There was such a command in my original solution to Lab 7, but not in ECardPanelLab9. You could add a similar command. Even better, you could have another command to "Restore Removed Item".

Another possibility would be to remove an item when the user shift-clicks it. But it would still be nice to have some way of restoring the item.


Keyboard "accelerators". Most programs have keyboard shortcuts for common menu commands, such as "Control-S" or "Command-S" for "Save". These shortcuts are called "keyboard accelerators" in Java, and they are covered in Section 13.3.5. They are not difficult to use. For example, to make "Control-S" into a shortcut for save, you just have to use the following command when you are creating the menu item (where "saveCommand" is the JMenuItem):

saveCommand.setAccelerator( KeyStroke.getKeyStroke("ctrl S") );

In the string, "ctrl" means that the shortcut uses the control key. which is most appropriate for Linux and Windows. For the command key on a Mac, you could use "meta" instead of "ctrl", but that won't work on Windows keyboards. (You can read the textbook to find out how to use the appropriate key for the computer that the program is running on, if you are interested.)


Warn about unsaved changes. Nice programs warn you if you give a command that will lose unsaved data, and they give you a chance to cancel the command. For example, if you choose the "New" or "Quit" command and the window has unsaved changed, you could ask, "There are unsaved changes. Do you really want to quit?". To ask the question, you can use JOptionPane.showConfirmDialog — just use code similar to the example of using that dialog in the doSave method.

But how do you know that there are unsaved changes? You can use a boolean instance variable, typically called dirty. Whenever you make a change to the program's data, you should set dirty = true. You have to be careful to do this for every change, including adding an item, changing the background, or modifying an overlay property. When you do a "New" or "Save" command, dirty should be reset to false.

You should also ask about saving changes if the user just clicks the close box on the window. Currently, that will end the program. That behavior is specified by the following line in main():

window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

You can delete that line and replace it with some code that will turn off that behavior and set up a WindowListener to respond when the user clicks the close box:

window.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
window.addWindowListener( new WindowAdapter() {
    public void windowClosing(WindowEvent evt) {
        panel.doQuit();
    }
});

This assumes that you have a method doQuit() in your program to carry out a "Quit" command.


Save an image. If you really wanted a program to make eCards for sending to friends, you would want to send them an image, not a file of obscure data that can only be viewed using your program. For that, you need a "Save Image..." command in the "File" menu. Section 13.1.5 discusses saving images. The function below is taken, with a small change, from that section. Unfortunately, to use it, you need a BufferedImage. A BufferedImage is an image stored in the computer's memory, not on the screen. It is possible to get a Graphics context for drawing on the image. In order to save the image from your program, you have to create a BufferedImage and draw the contents of the panel onto the image. This is easy! Simply call paintComponent() to draw to the BufferedImage instead of to the screen. It looks like this:

BufferedImage image = new BufferedImage(
         getWidth(),getHeight(),BufferedImage.TYPE_INT_ARGB);
paintComponent( image.getGraphics() );

And here's the method that you need to save the resulting image:

 /**
  * Attempts to save an image to a file selected by the user. 
  * @param image the BufferedImage to be saved to the file
  * @param format the format of the image, probably either "PNG" or "JPEG"
  */
 private void doSaveImage(BufferedImage image, String format) {
     if (fileDialog == null)
         fileDialog = new JFileChooser();
     fileDialog.setSelectedFile(new File("image." + format.toLowerCase())); 
     fileDialog.setDialogTitle("Select File For Saved Image");
     int option = fileDialog.showSaveDialog(this);
     if (option != JFileChooser.APPROVE_OPTION)
         return;  // User canceled or clicked the dialog's close box.
     File selectedFile = fileDialog.getSelectedFile();
     if ( ! selectedFile.getName().toLowerCase().endsWith("." + 
                                         format.toLowerCase())) {
            // Make sure the file extension agrees with the format.
         selectedFile = new File(selectedFile.getPath() + "." + 
                                         format.toLowerCase());
     }
     if (selectedFile.exists()) {  // Ask the user whether to replace file.
         int response = JOptionPane.showConfirmDialog( null,
                 "The file \"" + selectedFile.getName()
                 + "\" already exists.\nDo you want to replace it?", 
                 "Confirm Save",
                 JOptionPane.YES_NO_OPTION, 
                 JOptionPane.WARNING_MESSAGE );
         if (response != JOptionPane.YES_OPTION)
             return;  // User does not want to replace the file.
     }
     try {
         boolean hasFormat = ImageIO.write(image,format,selectedFile);
         if ( ! hasFormat )
             throw new Exception(format + " format is not available.");
     }
     catch (Exception e) {
         JOptionPane.showMessageDialog(this,
                 "Sorry, an error occurred while trying to save image.");
                 e.printStackTrace();
     }
 }