File Menu

The left-most menu for any editor is the File menu. Since this menu is as good a place to start as any, we'll build our initial Editor1 app around the File menu. Once we've mastered the File menu, adding further menu items will be a breeze.

Forms Designer Work

As always, create the Editor1 project using the Windows app builder. In Project Explorer, double-click Form1.java to open the Forms Designer. The majority of the work on Editor1 can be completed from within the Forms Designer.

Building the menus

To create the Editor1 app, begin by dragging a MainMenu control from the Toolbox to the Forms Designer. Unlike most controls, it doesn't really matter where you drop the MainMenu control—the menu will always appear along the top of the form. As soon as you drop the MainMenu control, a small box appears with the prompt "Type Here".

Start by typing &File. The ampersand tells the Forms Designer that the access key for this menu option is F—that is, the user can enter Alt+F rather than click the File menu. Instead of displaying the ampersand, the Forms Designer underscores the F.

As soon as you begin typing in the first Type Here box, two more Type Here boxes open up: one below for the first submenu item and one to the right for the next menu item. The Edit menu will go to the right of the File menu, but for the Editor1 app let's stick with the File menu. Add the File menu items for New, Open…, Save, Save As…, and Exit. Figure 7-1 shows the Editor1 menu items being edited.

Screenshot

Screenshot-1. The Forms Designer showing the menu editor for creating menu items.

Once the File menu is complete, return to the Properties window to give each submenu item a name related to the menu item's function, such as openFileMI for the Open menu item on the File menu. At the same time, edit the shortcut property to define shortcut keys for the two most common file commands: Ctrl+S for Save and Ctrl+X for Exit. Figure 7-2 shows the Ctrl+S key combination being assigned to the Save command.

Java Click to view at full size.

Screenshot-2. Setting a shortcut key enables the user to access the menu item from the keyboard with two keystrokes.

Building the dialog boxes

Thinking ahead, it is clear that the Open menu item on the File menu will need to open a dialog box from which the user can select the file to open. Fortunately, the Toolbox includes an OpenFileDialog control that creates just such a dialog box.

To add an Open file dialog box to the app, simply drag an OpenFileDialog control to the form. Like the MainMenu control, it doesn't matter where you put the object. This is because the dialog box is opened as a separate window. I named my OpenFileDialog control openFD. In addition, I set the default extension to .rtf; if the user enters a file name without an extension, the Open file dialog box will assign this extension. Finally I set the filter property to *.rtf, as shown in Figure 7-3. This sets the initial file filter to search for the RTF files that the editor specializes in. The Open file dialog box is created hidden. I will eventually have to add the code to unhide the Open file dialog box when it is needed.

Repeat this process for adding the dialog box to display when the user chooses Save As (or Save, if the file has never been saved). This time you'll use the SaveFileDialog control. You can use the same property settings as for the OpenFileDialog control, except the name of this object is saveFD.

Java Click to view at full size.

Screenshot-3. When setting up the OpenFileDialog control, remember to set the filter and the default extension to match the functionality of the app.

Finishing the design

Only a few final touches remain to the design of the form. First there is the addition of the all-important RichEdit control. Add this control as shown in and anchor it to all four walls of the form so that it will resize with the form.

Second, add a StatusBar control along the bottom of the form. We'll use this status bar to display the name and path of the file being edited, along with an indication of when the file has changed and therefore should be saved before exiting the app. It is possible to write text directly to the StatusBar object, but the output has a flat appearance. To get the 3-D effect of modern apps, it's better to add a StatusBarPanel object to the StatusBar control and write to that object. Select the panels property of the StatusBar control, and then click the ellipses button in the property setting box. This opens the Panel Editor dialog box, which enables the programmer to add panels. Add a single panel, and then close the dialog box. Set the StatusBar's showPanels property to true.

Then change the name of the status bar panel to outputPanel to reflect its function. I cleared out the default text within the panel; this will be filled in with the name of the file being edited. Setting the panel's autoSize property to Spring enables the panel to take up all available space within the status bar. Finally, setting the borderStyle to Sunken (the default) gives the toolbar the desired 3-D effect. Figure 7-4 shows the properties for the status bar panel as we've edited them.

Screenshot

Screenshot-4. To give the status bar a 3-D effect, first add a panel and then set its autoSize property to Spring and its borderStyle property to Sunken.

Code for Editor1

Even though the Forms Designer has done most of the work in building Editor1, there's still a sizable amount of code to be written. To keep Form1.java as simple as possible, I decided to create a separate class to maintain the rich edit and status bar controls. Let's examine this helper class before we get into the Form1 class.

NOTE
Don't be afraid to divide your project into separate classes as long as each new class has a clear purpose. Two small classes are easier to maintain than one large class.

RichEditController class

The primary job of the RichEditController class is to provide the load() and save() methods to load data from disk into the rich edit control and save the contents of the rich edit control back to disk. Along the way, RichEditController also keeps the status bar updated with the name of the file currently being edited and an indicator of whether the rich edit control is dirty or not. A rich edit control is dirty when its contents have been updated by the user since the last time the contents were saved to disk. Finally, RichEditController ensures that the edits are not inadvertently lost if the user forgets to save.

To create a new source file, choose New File from the MS Visual J Plus Plus File menu and then select Java File from the Visual J++ folder. This creates a .java file with a default name and adds the file to the project.

You should rename the file to match the name of the class it is to hold. This is a requirement if the class is public. You do this by choosing Save As from the File menu and supplying a new name for the file. I named my .java file and class RichEditController. The code listing for this module is as follows:

/**
 * This class provides the file load
 * and store operations we want for the
 * rich edit control.
 */
import com.ms.wfc.ui.*;
import com.ms.wfc.core.*;
import com.ms.wfc.io.*;
import com.ms.wfc.app.*;
public class RichEditController
{
 // form to which this controller is attached
 Form form;
 EventHandler saveHandler;
 // the rich edit control
 RichEdit re;
 // status bar panels
 StatusBarPanel fileNamePanel;
 // file name
 String fileName = "";
 /**
 * Create a controller to handle rich edit I/O.
 * * @param form - Parent Form
 * @param saveHandler - the File|Save event handler
 * @param re - RichEdit control
 * @param fileNamePanel - file name goes here
 */
 RichEditController(Form form,
 EventHandler saveHandler,
 RichEdit re,
 StatusBarPanel fileNamePanel)
 {
 this.form = form;
 this.saveHandler = saveHandler;
 this.re = re;
 this.fileNamePanel = fileNamePanel;
 // add event handlers:
 // record whenever text changes
 re.addOnTextChanged(
 new EventHandler(this.re_OnTextChanged));
 // intercept exit - if text changes, give user
 // the chance to save the text
 app.addOnappExit(
 new EventHandler(this.saveOnChange));
 }
 /**
 * Note that the rich edit control has changed.
 */
 void re_OnTextChanged(Object o, Event e)
 {
 if (!textChanged)
 {
 updateStatusBar(true);
 }
 }
 /**
 * Give the user a chance to save changes.
 */
 void saveOnChange(Object s, Event e)
 {
 // if the rich edit control hasn't changed…
 if (!textChanged)
 {
 // no worries
 return;
 }
 // otherwise, pop up a message box
 int yn = MessageBox.show("Save file?",
 "Text changed",
 MessageBox.ICONWARNING |
 MessageBox.OKCANCEL);
 // if the user selected OK…
 if (yn == DialogResult.OK)
 {
 // act like she selected File|Save
 saveHandler.invoke(form, e);
 }
 }
 /**
 * Set the name of the file.
 */
 void setFileName(String fileName)
 {
 this.fileName = fileName;
 updateStatusBar(false);
 }
 /**
 * Get the name of the current file.
 */
 String getFileName()
 {
 return fileName;
 }
 /**
 * Update the status bar with the name of the file
 * plus an indication of whether the file has changed
 * or not. Update the textChanged flag as well.
 * * @param textChanged - the new textChanged value
 */
 boolean textChanged = false;
 void updateStatusBar(boolean textChanged)
 {
 if (this.textChanged != textChanged)
 {
 this.textChanged = textChanged;
 fileNamePanel.setText(fileName
 + (textChanged ? " changed" : ""));
 }
 }
 /**
 * Open the default file name into the
 * rich edit control.
 */
 void load()
 {
 // first make sure it's okay to trash current contents
 saveOnChange(null, null);
 // open the specified file
 try
 {
 re.loadFile(fileName);
 updateStatusBar(false);
 }
 // first catch invalid file type
 catch(WFCInvalidArgumentException e)
 {
 re.setText("Invalid (non-RTF) file");
 }
 // handle all others the same way
 catch(Exception e)
 {
 re.setText(e.getMessage());
 }
 }
 /**
 * Save the contents of the current rich edit control to
 * the current file.
 */
 void save()
 {
 re.saveFile(fileName);
 updateStatusBar(false);
 }
}


The RichEditController class maintains a number of data members:

The RichEditController class constructor accepts a reference to the form, event handler, rich edit control, and file name status bar panel that the object is to control. After saving these locally the RichEditController() constructor establishes two event handlers.

The first event handler re_OnTextChanged() is invoked whenever the contents of the RichEdit object change. This method simply records the fact that the RichEdit object is dirty by setting textChanged to true. The updateStatusBar() method updates the status bar panel to inform the user that the file is now dirty. (If the textChanged flag is already set, the function returns without doing anything.)

The RichEditController constructor finishes by establishing saveOnChange() as the OnappExit event handler, meaning that WFC calls saveOnChange() before terminating the app. The purpose of saveOnChange() is to make sure that the user does not lose any edited data by exiting without saving. To do this, saveOnChange() first checks the textChanged flag. If its value is false, the RichEdit object isn't dirty and the function returns without taking any action. If the flag's value is true, the function pops up a message box.

NOTE
The message box is also known in some circles as an alert.

Unlike other control types, you do not create a MessageBox object yourself. Instead, you invoke the static method MessageBox.show() to create and immediately show the message box. The first two arguments to show() are the message and the title. The title appears in the title bar of the message box.

The last argument to the show() method is the style. This argument is a combination of the icon and the button styles (combined using the OR operator). The different icon styles display different graphical symbols in the message box; the ICONWARNING style displays a yield sign. The button styles each display a different set of buttons; the OKCANCEL button style creates an OK button and a Cancel button in the message box.

Since message boxes are always modal, the call to show() does not return until the user has selected one of the buttons on the message box. The show() method returns the value OK if the user selects OK.

If show() returns OK, saveOnChange() calls saveHandler.invoke(). This invokes the EventHandler passed to the class constructor as the Save menu item handler. Calling invoke() this way simulates the event actually occurring, exactly as if the user had clicked the Save menu item.

The getFileName() and setFileName() methods return the current file name and update the current file name, respectively. The setFileName() method also updates the status bar to reflect the new name.

The updateStatusBar() method uses the textChanged flag. Whenever the value of this flag changes, updateStatusBar() updates the name of the current default file on the status bar. If the RichEdit object is dirty, updateStatusBar() tacks the string "changed" onto the end of the file name before writing it to the fileNamePanel object.

The load() and save() methods are similar to like-named methods in the rich edit control example in .

Form1 class

Armed with the RichEditController class, the code for Form1 is almost anticlimactic:

import com.ms.wfc.app.*;
import com.ms.wfc.core.*;
import com.ms.wfc.ui.*;
import com.ms.wfc.html.*;
import com.ms.wfc.io.*;
/**
 * This class represents the File section of our
 * RTF editor.
 */
public class Form1 extends Form
{
 // rec controls the RichEdit object and the status bar
 RichEditController rec;
 public Form1(String[] args)
 {
 // Required for Visual J Plus Plus Form Designer support
 initForm();
 // build a controller for the rich edit control; this
 // will handle all of the I/O duties
 rec = new RichEditController(this,
 new EventHandler(this.saveFileMI_click),
 richEdit, outputPanel);
 // if there is a file name present…
 String defaultDirectory;
 if (args.length == 1)
 {
 // …then load it…
 rec.setFileName(args[0]);
 rec.load();
 // and record the file's directory;
 defaultDirectory = File.getDirectory(args[0]);
 }
 // otherwise, start in the current directory
 else
 {
 defaultDirectory = File.getCurrentDirectory();
 }
 // now set the default directory as the initial
 // directory for both the open and save dialog boxes
 openFD.setInitialDir(defaultDirectory);
 saveFD.setInitialDir(defaultDirectory);
 }
 /**
 * Form1 overrides dispose so it can clean up the
 * component list.
 */
 public void dispose()
 {
 super.dispose();
 components.dispose();
 }
 private void newFileMI_click(Object source, Event e)
 {
 // make sure it's okay to trash current contents
 rec.saveOnChange(null, null);
 // wipe out the contents of the rich edit
 richEdit.setText("");
 // wipe out the old file name
 rec.setFileName("");
 }
 private void openFileMI_click(Object source, Event e)
 {
 // make sure it's okay to trash current contents
 rec.saveOnChange(null, null);
 // open the Open dialog; if user selects OK…
 if (openFD.showDialog() == DialogResult.OK)
 {
 // open the file
 rec.setFileName(openFD.getFileName());
 rec.load();
 } }
 private void saveFileMI_click(Object source, Event e)
 {
 // if there is no current file…
 if (rec.getFileName().equals(""))
 {
 // treat this as a Save As
 saveAsFileMI_click(source, e);
 return;
 }
 // save contents into current file name
 rec.save(); }
 private void saveAsFileMI_click(Object source, Event e)
 {
 if (saveFD.showDialog() == DialogResult.OK)
 {
 rec.setFileName(saveFD.getFileName());
 rec.save();
 }
 }
 private void exitFileMI_click(Object source, Event e)
 {
 app.exit(); }
 /**
 * NOTE: The following code is required by the Visual J Plus Plus form
 * designer. It can be modified using the form editor. Do not
 * modify it using the code editor.
 */
 Container components = new Container();
 MainMenu mainMenu = new MainMenu();
 MenuItem fileMI = new MenuItem();
 MenuItem newFileMI = new MenuItem();
 MenuItem openFileMI = new MenuItem();
 MenuItem saveFileMI = new MenuItem();
 MenuItem saveAsFileMI = new MenuItem();
 MenuItem exitFileMI = new MenuItem();
 OpenFileDialog openFD = new OpenFileDialog();
 SaveFileDialog saveFD = new SaveFileDialog();
 RichEdit richEdit = new RichEdit();
 StatusBar statusBar = new StatusBar();
 StatusBarPanel outputPanel = new StatusBarPanel();
 private void initForm()
 {
 // …created by the Forms Designer…
 }
 /**
 * The main entry point for the app. */
 public static void main(String args[])
 {
 app.run(new Form1(args));
 }
}


I added the variable args containing the program arguments to the constructor for Form1 so that if a file name is passed as the first argument to the program, Form1() can load the file immediately. This addition is primarily to support initial drag-and-drop capability as explained in .

Upon startup, the Form1() constructor invokes initForm() to allow the Forms Designer_generated code to create the form. Upon return from initForm(), Form1() creates the RichEditController object described earlier. The EventHandler object passed to RichEditController points to the method saveFileMI_click().

Once the RichEditController object has been created, it can be used to load the file name contained in args[0], if one is present. If there is a file name contained in args[0], the string variable defaultDirectory is set to the path to that file; otherwise, defaultDirectory is set to the current directory. This defaultDirectory value is then used as the initial directory for both the Open file dialog box, openFD, and the Save As file dialog box, saveFD. (If we don't set the directory, the Windows default directory will be used in both dialog boxes, which I find inconvenient.)

The next five methods (following the dispose() method) are each attached to their respective File menu items. For example, double-clicking the New menu item in the Forms Designer created the method newFileMI_click(). We added code to this method so that it first calls saveOnChange() to make sure that the user isn't mistakenly wiping out any new text that might already be in the rich edit control. If the RichEdit object is empty, this call has no effect. The newFileMI_click() method then clears out the text within the RichEdit object and clears out the current file name.

The openFileMI_click() method, which is invoked from the Open menu item, first calls saveOnChange() and then opens the Open file dialog box openFD. Calling showDialog() makes the openFD dialog box visible. Since this dialog box is modal, control does not return from showDialog() until the user chooses the OK or Cancel button. If openFD.showDialog() returns the value OK, then openFileMI_click() sets the current file name and then loads that file into the RichEdit object.

The saveFileMI_click() method first checks to see if there is a current file name. If there is, saveFileMI_click() invokes rec.save() to save the contents of the RichEdit object to the file. If there is no current file, saveFileMI_click() calls saveAsFileMI_click(). Thus, when there is no file name, clicking Save is identical to choosing Save As.

The saveAsFileMI_click() method opens the saveFD Save As file dialog box. The Save As file dialog box is very similar to the Open file dialog box openFD. If the dialog box object returns an OK indication, saveAsFileMI_click() retrieves from saveFD the file name chosen by the user and stores it as the default current file name before calling rec.save().

The exitFileMI_click() method simply calls app.exit() to terminate the program. (Remember that before the app exits, RichEditController.saveOnChange() will be invoked if the RichEdit object is dirty, to allow the user to save any modifications.)

How well does it work?

Screenshot-5 shows Editor1 with the File menu open. From the status bar, you can see the path to the file currently being edited and that the contents of the rich edit window are dirty.

Screenshot

Screenshot-5. The Editor1 app with a complete set of File menu items.

Screenshot-6 shows the warning message that appears when you try to open a new file without saving the modified file.

If you click the OK button, Editor1 saves the file and then opens the Open file dialog box as shown in Figure 7-7. (If the file is new and has never been saved, clicking OK will first open the Save As dialog box, allowing you to specify a name and location to save the new file to before opening the Open dialog box.)

What is needed now to turn this initial version into a more full featured editor are a couple of additions, such as an Edit menu, a toolbar, and a context menu.

Screenshot

Screenshot-6. The Text Changed message box warns the user that the edited contents of the rich edit window are potentially about to be lost.

Java Click to view at full size.

Screenshot-7. Editor1 uses the standard Open dialog box to allow you to select a file to open.

WHY PASS AN EVENTHANDLER OBJECT?


You may wonder why I bothered passing an EventHandler object to the constructor for the RichEditController class. Form1 created an EventHandler object from saveFileMI_click() and then passed it to the RichEditController() constructor. The EventHandler object was used in only one place and that was to call saveFileMI_click() to save the current file before loading a new one. Why not just call saveFileMI_click() directly?

NOTE
Using the EventHandler class in this way is very much like registering a callback function in C or C++.

A service class, such as RichEditController, should make as few assumptions about other user classes as possible. However, RichEditController must make assumptions about library classes such as those that make up WFC. For example, when I wrote RichEditController I knew that the method StatusBarPanel.setText() exists and I knew what its arguments are.

Because Form1 isn't part of the Java class libraries, you shouldn't assume that any Form1 object passed will have a saveFileMI_click() method or that the method's purpose is to process a Save command. It is much better for the author of RichEditController to ask the outside world for a method that performs the Save function.

In effect, that's exactly what the constructor of RichEditController is doing. It is saying, "I need a reference to your Form object and a reference to your Save handler." Its presence in the constructor's argument list alerts you that such a routine is required. This approach makes no assumptions about what Save routine might be called.

Comments