CPSC 225, Spring 2019
Lab 11: Files

This lab is the second part of the "eCard" project, which was begun in Lab 10. The completed project is due in two weeks. For next week's lab (which is the day after a test), you can either work on your final project or continue work on the eCard project.

You should submit your work into a folder named lab11 inside your homework directory. That should be done by the start of lab on Tuesday, April 23. You must submit ECardDesigner.java. If you have written any other Java files for the project, you should also submit them. It is not necessary to submit the images and textures folders unless you added your own image files to them.

About the final project proposal: The plan for your final project is due this Friday, April 12. It should be turned in on paper in class. According to the final project information that was handed out earlier, "The plan must include a list of the major classes that will be used in the project, with a description of what each class will do. It should describe the GUI or other user interface, if applicable. It should identify any files or other external resources that will be used or generated. And it should identify any obstacles that you foresee to completing the project, such as aspects that you still don't know how to implement."

Save and Open Commands

You should start the second part of the project by adding commands "Save..." and "Open..." to the "File" menu. The "..." at the end of a command typically means that a dialog box will open when the command is selected. In this case, the dialog box is a FileChooser that will let the user specify the file to be saved or opened. Some typical methods for saving and opening files are given below (but, of course, without the part that reads/writes the actual data). You should copy-and-paste the code into your program, and you should arrange that the "Save..." and "Open..." commands will call these methods. You can test that that works before proceeding.

    private void doSave() {
        FileChooser fileDialog = new FileChooser(); 
        fileDialog.setInitialFileName("eCard.txt");
        fileDialog.setInitialDirectory( 
                        new File( System.getProperty("user.home")) );
        fileDialog.setTitle("Select File to be Saved");
        File selectedFile = fileDialog.showSaveDialog(window);
        if ( selectedFile == null )
            return;  // User did not select a file.
        /* Note: At this point, user has selected a file AND, if it
         *   exists, has confirmed that it is OK to erase the file. */
        PrintWriter out; 
        try {
            out = new PrintWriter( selectedFile );
        }
        catch (Exception e) {
               // Most likely, user doesn't have write permission
            Alert errorAlert = new Alert(Alert.AlertType.ERROR,
                    "Sorry, but an error occurred while\n" +
                    "trying to open the file for output.");
            errorAlert.showAndWait();
            return;
        }
        try {
            // TODO Write everything to the selected file.
            
            out.close();
            if (out.checkError()) // (check for output errors)
                throw new IOException("Error check failed.");
        }
        catch (Exception e) {
            Alert errorAlert = new Alert(Alert.AlertType.ERROR,
                    "Sorry, but an error occurred while\n" +
                    "trying to write data to the file:\n" + e);
            errorAlert.showAndWait();
            e.printStackTrace();
        }    
    }

    private void doOpen() {
        FileChooser fileDialog = new FileChooser();
        fileDialog.setInitialDirectory( 
                      new File( System.getProperty("user.home")) );
        fileDialog.setTitle("Select File to be Opened");
        File selectedFile = fileDialog.showOpenDialog(window);
        if ( selectedFile == null )
            return;  // User did not select a file.
        /* Note: At this point, user has selected a file. */
        Scanner in; 
        try {
            in = new Scanner( selectedFile );
        }
        catch (Exception e) {
                // Maybe the user doesn't have read permission.
            Alert errorAlert = new Alert(Alert.AlertType.ERROR,
                    "Sorry, but an error occurred while\n" +
                    "trying to open the file for input.");
            errorAlert.showAndWait();
            return;
        }
        try {
            // TODO Read everything from the selected file.
            
            in.close();
        }
        catch (Exception e) {
            Alert errorAlert = new Alert(Alert.AlertType.ERROR,
                    "Sorry, but an error occurred while\n" +
                    "trying to read data from the file:\n" + e);
            errorAlert.showAndWait();
            e.printStackTrace();
        }    
    }

Save and Restore the Background

For saving a file, you will work with the PrintWriter, out, in the doSave() method that you copied into your program. For opening and reloading a file, you will work with the Scanner, in, in the doOpen() method.

I suggest that as a warm-up, you make it possible to save and load the background of the eCard. The background is an object of type Paint. The Color class that we have been using is a subclass of Paint. In fact, any subclass of Paint can be used for stroking and filling shapes. ImagePattern is a subclass of Paint that copies pixels from an image.

In the program, the background fill is stored in an instance variable named backgroundPaint, of type Paint The background paint can be either a Color or an ImagePattern. When you save the eCard data to a file, you need to include enough information so that you can recreate the exact same background when the file is reloaded into the program. Now, you know backgroundPaint is either a Color or a ImagePattern, but you don't know which one it is. You can find out with the instanceof operator, which is used to test whether an object belongs to a certain class. The code will something like

if ( backgroundPaint instanceof Color ) {
    Color c = (Color)backgroundPaint;  // type-cast to type Color
    // save background color info to file
}
else {
    //save background pattern info to file
}

This means that, first of all, you need to save something to indicate what kind of background fill is used, color or pattern. That could be, for example, a boolean value, or a word. Then you have to save the data for the background. For a color you might save four numbers representing the red, green, blue, and opacity values from the background color. For a pattern, you need to save the name of the image resource file that was used to create the pattern. (Unfortunately, the program does not currently have any way of remembering what image file was used to create a background pattern; you will need to add an instance variable of type String to record the image file name for the background pattern.) So, the line in the file that records the background might look something llke

     color 1.0 0.85 0.85 1.0
or
     pattern celtic-knot.jpg

When you reload the file, the first token that you read tells you which type of background is used. And it also tells you what additional data you will need to read in order to create the Paint object. For example, once you know that the background is a color, you can create the background color with

backgroundPaint = Color.color(in.nextDouble(), in.nextDouble(),
                                  in.nextDouble(), in.nextDouble());

Note that it's OK to assume that the data in the file is legal and just go ahead and read it. If it's not, in fact, a legal eCard file, an exception will almost certainly occur, which will be the correct response to an invalid file.

One final note about backgrounds: in order to get the new background to appear in the window, you have to call repaintBackground().

And a note about numbers in the eCard data files. Although we have been thinking of things like coordinates and the width and height of shapes as integers, in fact almost every numeric value in JavaFX is actually of type double. That means that when you write the value to a file, it will be written as a floating point number, and when you read the value with a Scanner, in, you should read it using in.nextDouble() rather than in.nextInt().

Save and Restore Items

Of course, you also need to save and restore the ArrayList<Node> that holds all the items on the eCard. We discussed in class how to save lists in general. In this program, an item in the list is of type ImageView, Text, Rectangle, or Ellipse. You could use instanceof to test the type of an item. For each item, you need to save all the information necessary to restore it, including an indication of what kind of item it is.

Every Node, item, has a position in the eCard, which must be saved to a the file. Its coordinates can be obtained by calling item.getLayoutX() and item.getLayoutY(). The position will have to be restored when the file is loaded using item.setLayoutX(x) and item.setLayoutY(y).

Similarly, all of the other attributes of the items will have to be saved and restored. Each item has its own set of attributes. For example, a Text item, t, has the actual string of text as an attribute, which can be manipulated using t.getText() and t.setText(str). By the way, text requires special handling when you are reading it in using a scanner. Since a string of text can contain blanks, it is not a single token and cannot be read using in.next(). It can be read using in.nextLine(), but you have to be careful: If the next thing in the input is an end-of-line when you call in.nextLine(), it will only read the end-of-line. It might be necessary to call in.nextLine() once to read the end-of-line and a second time to read the text.

When you work with an ImageView item, you will have a problem similar to the one with background patterns: an ImageView does not remember the name of the image file that was used to create it. JavaFX has a clever, but somewhat klutzy, way to deal with this. Every Node can stash an associated data object in the node. That data is not used by the system but is purely for you, the programmer. When an ImageView, img, is created from an image file, you can stash the file name by calling img.setUserData(filename). When you need the file name that was stashed in an ImageView, i, you can get it by calling i.getUserData(). The return value is of type Object but it can be type-cast to String, using (String)i.getUserData().

There is a similar problem when saving a Text item: The data that was used when making the Font is not easy to retrieve. This data consists of the size of the font, whether it is bold, and whether it is italic. You might want to stash this information in the Text item's userdata. (My program always uses "Times New Roman" for the font, so I didn't have to save that.)

There is a lot of detail here. You will need to work carefully and incrementally. I suggest trying to save and restore just one type of item at a time, and debugging that process before proceeding to the next type of item. There might be more issues that I have forgotten to mention here. You will need to work steadily on the project and ask for help when you need it! Do not try to wait until the last minute to do this project!