Java GUI Made Easy part 2

Adding Menus

Ok, let’s get going with the next part. The only new part in the constructor of the main class is:

        createMenus();

As the method name indicates this is where we set up the menus for our app. And here it is:

    private void createMenus() {
        JMenuBar bar = new JMenuBar();
        parent.setJMenuBar(bar);
        
        FileAction fileAction = new FileAction(this);
        JMenu fileMenu = new JMenu(fileAction); 
        bar.add(fileMenu);
        fileMenu.add(new JMenuItem(fileAction.new NewAction()));
        fileMenu.add(new JMenuItem(fileAction.new OpenAction()));
        fileMenu.add(new JMenuItem(fileAction.new SaveAction()));
        fileMenu.add(new JMenuItem(fileAction.new SaveAsAction()));
        fileMenu.addSeparator();
        fileMenu.add(new JMenuItem(fileAction.new PageSetupAction()));
        fileMenu.add(new JMenuItem(fileAction.new PrintAction()));
        fileMenu.addSeparator();
        fileMenu.add(new JMenuItem(fileAction.new ExitAction()));
        
    }

There are 3 classes that we use to set up our menu system. They are JMenuBar, JMenu, JMenuItem. The JMenuBar class is used to attach the whole menu system to the application frame. JMenuItem represents all the selectable items on your menu that the use clicks to perform an action. Jmenu holds all the JMenuItem objects. You can also put a JMenu in another JMenu. This is how you would create a sub-menu within a menu.
First you create the JMenuBar and set it to your parent window (JFrame). Next create an action for the top level file menu. I’ll talk about how actions work shortly. Create the file menu object as a JMenu and pass the file action into the constructor. Add the file menu to the menu bar. Once the top level menu is set up we add a new menu item for each selectable menu on it. For each item created we also pass in an action specific to that menu. There is also a special menu type that we add with addSeparator. This adds a visual separator bar between menu groups.
Time to look at what all these action classes are about. First let’s look at the Action interface. Depending how complex your application gets you may end up having multiple ways to cause the same thing to happen. For example you might have a File menu with a Save option. You could also have a toolbar with a floppy disk icon for save as well. Instead of coding separate action listeners and tooltips and icons and short cut keys for each, you can instead set up an Action. There are 2 ways to go about it. You can implement the Action interface, which has 7 methods to implement. Another option is to extend the AbstractAction class. By extending AbstractAction you can leave a lot of the basic details to the parent class and you only have to worry about 2 things. One is the actionPerformed method that gets called when your actionable item is selected. The other is setting up the any values that you want on all your actions, eg name, shortcut key, tooltip, etc… Check out the API docs on javax.swing.Action for more details on what options you can set.
Now we can look at the FileAction class that we created.
public class FileAction extends AbstractAction {

    private static final long serialVersionUID = -3139014331682985614L;

    private JNotepad jNotepad;
    /**
     * Set up values for the action.
     * 
     * @param jNotepad Reference to the application so we can apply menu 
     *            selections back to the application.
     */
    public FileAction(JNotepad jNotepad) {
        this.jNotepad = jNotepad;
        putValue(NAME, "File");
        putValue(MNEMONIC_KEY, KeyEvent.VK_F);
    }
    @Override
    public void actionPerformed(ActionEvent arg0) {
        //No actions needed on a top level menu.
    }

As you can see we extend AbstractAction. In the constructor we take a reference to our application’s main class so we can perform call backs on each type of action. Here we also use 2 of the standard action keys to set up values. The NAME key represents what is visible to the user as the name of your action. Any actionable item that uses text to display the command name, such as menus and buttons, will display this name to the user. MNEMONIC_KEY is the shortcut key that will be used when the user presses the ALT key to try and select a menu or menu item. In this case ALT+F will open the file menu.
Looking at actionPerformed you can see there is no code in there. The reason for this is the File menu is just that a menu, not a selectable item. Menu’s don’t have any type of action to perform they are just containers for items.

    public class NewAction extends AbstractAction {
        private static final long serialVersionUID = -684985649960046392L;

        /**
         * 
         */
        public NewAction() {
            putValue(NAME, "New");
            putValue(MNEMONIC_KEY, KeyEvent.VK_N);
            putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK));
        }
        @Override
        public void actionPerformed(ActionEvent e) {
            jNotepad.newDocument();
        }
        
    }

The NewAction class is defined as an inner class inside FileAction. I do this as a way to keep related actions together and to try and keep the code a little neater. This is why to set up all the other actions you saw an object reference, fileAction.new NewAction(), used to create the actions for the sub-menus under the File menu. In the constructor for NewAction there is a new key that we haven’t seen yet. ACCELERATOR_KEY is used to set up a short cut that you can use without opening the menu up. You have to pass a KeyStroke object in as the value for this key. The KeyStroke class has some static helper methods set up to make it easier. In this case we set up CTRL+N will trigger the NewAction. Now in the actionPerformed for this action you can see the first callback to the main application to perform any operations required for this action. I’ll show you one more action and then look back at the main class for the operations that are being performed.

    public class OpenAction extends AbstractAction {
        private static final long serialVersionUID = -684985649960046392L;

        /**
         * 
         */
        public OpenAction() {
            putValue(NAME, "Open...");
            putValue(MNEMONIC_KEY, KeyEvent.VK_O);
            putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK));
        }
        @Override
        public void actionPerformed(ActionEvent e) {
            jNotepad.open();
        }
        
    }

Seen one action you’ve seen them all. It can get pretty redundant. One convention to note here is in the name you can see an ellipses(…) after the name. This convention tells the user that if they select this item that a new window or dialog will open up. In this case we will display a file choosing dialog. Everything else here is the same as the previous action. Set up the mnemonic and accelerator keys. Then in the actionPerformed call back to the main application the appropriate method to perform all the operations required.

    public void newDocument() {
        //TODO add check for changed document to popup dialog to user 
        //about saving current document
        textArea.setText("");
    }

Here is the first callback method in the main application. Since we are not getting to complicated yet we will just leave ourselves a note to add in a dirty check to see if the user is editing an unsaved document. If they are we will popup a dialog to check about saving. Since we are not doing that yet all we have to do is clear the current document by setting it to a zero length String.

    public void open() {
        JFileChooser chooser = new JFileChooser(Utils.lastFilePath);
        chooser.addChoosableFileFilter(new FileFilter() {
            
            @Override
            public String getDescription() {
                return "Text files(*.txt)";
            }
            
            @Override
            public boolean accept(File f) {
                if(f.isDirectory() || f.getAbsolutePath().toLowerCase().endsWith(".txt")) {
                    return true;
                }
                else {
                    return false;
                }
            }
        });
        if(chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
            File chosenFile = chooser.getSelectedFile();
            Utils.lastFilePath = chosenFile.getParent();
            Utils.lastFileChosen = chosenFile.getName();
            try {
                InputStreamReader in = new InputStreamReader(new FileInputStream(chosenFile));
                CharBuffer buff = CharBuffer.allocate((int)chosenFile.length());
                in.read(buff);
                in.close();
                buff.rewind();
                textArea.setText(buff.toString());
            } 
            catch (IOException e) {
                JOptionPane.showMessageDialog(this, "There was a problem trying to load the file " + chosenFile.getAbsolutePath(), "Error Loading File", JOptionPane.ERROR_MESSAGE);
                e.printStackTrace();
            }
            
        }
    }

Next we have the open method that is called if the user selects an open action such as File > Open from the menu. The first thing you see here is the JFileChooser class. This class provides a native dialog box to open or save files with. The standard file chooser dialog comes with a filter that allows all files to be selected. We also want to add one of our that is specific to our applicatino. In our case we want to add a filter for text files that us the extension .txt. On the second line we call addChoosableFileFilter from the file chooser instance. We then create an anonymous class that implements the FileFilter interface. This shows the use of anonymous classes. You don’t have to come up with a new name for a class and you don’t have to clutter up your namespace with a new name you might want to use later. If you don’t have to reference an instance of a class that might be a good candidate for an anonymous class. In the class for the file filter you provide the logic that selects which files to display. You want to check for all files that end in .txt no matter which case but you also want to make sure you allow all directories through your filter. If you don’t allow directories through then you can’t navigate the file system to look for your files. Once the filter is set you can call the showOpenDialog to display the dialog to the user. You can pass in null as the argument or since it is being called from a swing component implementation you can pass in this as the componet argument. Calling this method will show a modal dialog that will block untill the user accepts or cancels the dialog. If the user clicks the the button to approve the selection then the method will return APPROVE_OPTION. Otherwise CANCEL_OPTION is returned if the dialog is closed in any other way.
The file chooser can handle selection of multiple files or single files. You can use setMultiSelectionEnabled to make this selection. The default is single file selection. For single file selection call getSelectedFile to get the selection from the user in the form of a java.io.File. This will represent the full path to access the selected file. We want to this up into the file name and the path. We want the path so we can save the last used path as a convenience for the user. This will allow us to do any future file dialogs starting in the last path used.
Now we want to read the file in for editing. To keep things simple we will not look at file encodings. That is a subject all on its own. Since we are only dealing with text files we can use a class that is built to deal with text. The java.io.InputStreamReader class is the one we will use. You can also use java.nio classes to read files but they are a little more complicated than just using plain old streams. The InputStreamReader allows us to read in the text file all in one go. We use the java.nio.CharBuffer class to temporarily store everything we read in. The thing you have to remember about buffers is that after you fill them you will want to rewind them to make sure the pointer is at the beginning of the data. So don’t forget the rewind before copying the contents. After the read operation just copy the CharBuffer contents into the textarea.
The last thing you want to do here is inform the user if there is a problem. We set up a try/catch block in case something happens while we are reading the file. If there is a problem you can notify the user. At this point if you were making a production application you would also want to have some logging. For now you can just print the stack trace, since you are the only user.

    public void save(boolean showDialog) {
        boolean save = true;
        if(Utils.lastFileChosen == null || showDialog) {
            save = openSaveDialog();
        }
        
        if(save) {
            try {
                FileWriter out = new FileWriter(Utils.lastFilePath + "/" + Utils.lastFileChosen);
                out.write(textArea.getText());
                out.close();
            } 
            catch (IOException e) {
                JOptionPane.showMessageDialog(this, "There was a problem trying to save the file " + Utils.lastFilePath + "/" + Utils.lastFileChosen, "Error saving File", JOptionPane.ERROR_MESSAGE);
                e.printStackTrace();
            }
        }
    }

For the save option, we’ll start out assuming we can save by setting the boolean value to true. Next we’ll check to see if we already have a file loaded. If not we’ll call up the save dialog to get a filename to use. We’ll also show the dialog if the method was called with the showDialog argument set to true. Here we see the importance of store the filename from the last document loaded. If the user chooses save from the file menu and already has a document loaded we will just overwrite the same file with the new edited content. The openSaveDialog method will show the user the save dialog and return true if the user selected a file.
If we have determined that it is OK to save the file, we create a stream to write out the content. In this case we will use the java.io.FileWriter stream to simplify writing to a text file. As with loading we will also not worry about character sets. As with any other area where external API calls might throw exceptions we want to show the user a dialog box in case of an exception.

        public void actionPerformed(ActionEvent e) {
            jNotepad.save(true);
        }

The save as feature is pretty simple. Using the DRY (Don’t Repeat Yourself) principle we just call existing save method, from the action, with the appropriate argument for the save dialog.
The printing is fairly complex so I will leave it until the end.

        public void actionPerformed(ActionEvent e) {
            jNotepad.exit();
        }

For the exit menu action we just call the existing exit method that was defined in part 1.

Alright I’ve covered most items in the File menu. The next part will cover all the items in the edit menu. Printing in Java can get complicated so I will leave that until the end.

JNotepad.java
FileAction.java
Utils.java

About these ads

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Follow

Get every new post delivered to your Inbox.

%d bloggers like this: