In our last lesson, we wrote the CPMenuBar class entirely except for hooking up the data management operations on the File menu: Open, Save, Save As, Close, and Delete. In this lesson, we’ll complete and test the data management operations, leaving us with a fully functional Cartesian plane application.
GitHub repository: Cartesian Plane Part 17
Previous lesson: Cartesian Plane Lesson 17 Page 15: Data Organization, GUI, CPFrame
Refactoring
To make things more interesting, we’ve added a few more buttons to the File menu in this lesson. They are:
- New: closes any currently open equation and instantiates a new one.
- Close: closes the currently open equation.
- Delete: deletes the currently open file.
CPMenuBar Data Management Operations
This lesson is about completing the New, Open, Save, Save As, Close, and Delete operations initiated from the File menu of our application’s menubar. Let’s start by reviewing how those operations work and, in particular, the various states that our application may occupy with respect to those operations. In this context, state refers to:
- The value of the enabled property for each File menu button.
- The value of the open-equation (DM_OPEN_EQUATION_PN), open-file (DM_OPEN_FILE_PN), and modified (DM_MODIFIED_PN) properties.
- The value of the enclosing CPFrame’s equation property.
- Whether or not the CPFrames subpanels are considered enabled.
Caveat: For simplicity, we are, at least for now, ignoring the following behaviors that I would usually expect in these kinds of operations:
- If the operator initiates a new, open, or close operation while changes are pending for a currently open equation, we should probably post an “Are you sure?” dialog.
- If the operator chooses to delete a file, we should probably post an “Are you sure?” dialog.
⏹ CPFrame Properties
Any time an equation is open, it must be registered with the enclosing CPFrame, and the interactive components in the frame’s subpanels must be enabled. If there is no open equation, the enclosing CPFrame’s equation property must be null, and the interactive components in the frame’s subpanels must be disabled.
⏹ New Operation
This operation creates a new equation (for the moment, we are limiting ourselves to Exp4jEquations). The operator can initiate a new operation anytime, so this menu button should always be enabled. At the start of the operation, the open button must be enabled (because it is always enabled), but the configuration of other state properties is unpredictable. At the completion of the operation, the Save, Close, and Delete buttons must be disabled, and the save-as button and the subpanels of the frame must be enabled. The open-equation property must be true, and the open-file and modified properties must be false. (See also Caveat above.)
| New Operation | |||||||
|---|---|---|---|---|---|---|---|
| Before | After | ||||||
| T | F | N | T | F | N | ||
| File/New | ● | ; | ● | ||||
| File/Open | ● | ● | |||||
| File/Save | ● | ● | |||||
| File/Save As | ● | ● | |||||
| File/Close | ● | ● | |||||
| File/Delete | ● | ● | |||||
| Panels | ● | ● | |||||
| OPEN_EQUATION | ● | ● | |||||
| MODIFIED | ● | ● | |||||
| OPEN_FILE | ● | ● | |||||
⏹ Open Operation
This operation opens an existing file containing an equation specification and instantiates the equation described. An open operation can be initiated anytime, so this menu button should always be enabled. We must be prepared for the operator to cancel the operation after initiating it and for the operation to fail.
The application state must remain unchanged if the operation fails or the operator cancels it.
| Open Operation | |||||||
|---|---|---|---|---|---|---|---|
| Before | After | ||||||
| T | F | N | T | F | N | ||
| File/New | ● | ; | ● | ||||
| File/Open | ● | ● | |||||
| File/Save | ● | ● | |||||
| File/Save As | ● | ● | |||||
| File/Close | ● | ● | |||||
| File/Delete | ● | ● | |||||
| Panels | ● | ● | |||||
| OPEN_EQUATION | ● | ● | |||||
| MODIFIED | ● | ● | |||||
| OPEN_FILE | ● | ● | |||||
If the operation is completed successfully, the save-as, close, and delete buttons must be enabled; the save button must be disabled; the open-file and open-equation properties must be true; the modified property must be false; and all the subpanels of the application frame must be enabled. (See also Caveat above.)
⏹ Save Operation
This operation saves a modified equation to the currently open file. Therefore, this button can be enabled only if the open-equation property is true, the open-file property is true, and the modified property is true. Since an equation is open, the panels of the application frame and the save-as and close buttons must be enabled. Since a file is open, the delete button must be enabled. After the operation completes, the modified property changes to false, and the save button is disabled.
| Save Operation | |||||||
|---|---|---|---|---|---|---|---|
| Before | After | ||||||
| T | F | N | T | F | N | ||
| File/New | ● | ; | ● | ||||
| File/Open | ● | ● | |||||
| File/Save | ● | ● | |||||
| File/Save As | ● | ● | |||||
| File/Close | ● | ● | |||||
| File/Delete | ● | ● | |||||
| Panels | ● | ● | |||||
| OPEN_EQUATION | ● | ● | |||||
| MODIFIED | ● | ● | |||||
| OPEN_FILE | ● | ● | |||||
⏹ Save As Operation
This operation saves an open equation to a file of the operator’s choice. The open-equation property must be true for this operation to be available. Since an equation is open, the subpanels of the application frame must be enabled, and the close button must be enabled. The state of the modified button can be true or false. A file may or may not be open, so the states of the open-file property and the save and delete buttons can be anything. If the operation fails or if the operator cancels the operation, the application state must remain unchanged. If the operation is completed successfully, the state of the application must be configured as follows:
| Save-As Operation | |||||||
|---|---|---|---|---|---|---|---|
| Before | After | ||||||
| T | F | N | T | F | N | ||
| File/New | ● | ; | ● | ||||
| File/Open | ● | ● | |||||
| File/Save | ● | ● | |||||
| File/Save As | ● | ● | |||||
| File/Close | ● | ● | |||||
| File/Delete | ● | ● | |||||
| Panels | ● | ● | |||||
| OPEN_EQUATION | ● | ● | |||||
| MODIFIED | ● | ● | |||||
| OPEN_FILE | ● | ● | |||||
- The open-file property must be true.
- The open-equation property must be true.
- The modified property must be false.
- The save button must be disabled.
- The save-as button must be enabled.
- The close button must be enabled.
- The delete button must be enabled.
- The application frame subpanels must be enabled.
⏹ Close Operation
This operation closes the currently open equation, so this button can only be enabled if the open-equation property is true. If a file is open, it must be closed. The open-file and modified properties can be in any state. Since an equation is open, the application frame subpanels and the save-as and close buttons must be enabled. The save and delete buttons can be in any state. After the operation completes, no file or equation will be open, so the application subpanels must be disabled, as must the save, save-as, close, and delete buttons. The open-equation, open-file, and modified properties must all be false.
| Close Operation | |||||||
|---|---|---|---|---|---|---|---|
| Before | After | ||||||
| T | F | N | T | F | N | ||
| File/New | ● | ; | ● | ||||
| File/Open | ● | ● | |||||
| File/Save | ● | ● | |||||
| File/Save As | ● | ● | |||||
| File/Close | ● | ● | |||||
| File/Delete | ● | ● | |||||
| Panels | ● | ● | |||||
| OPEN_EQUATION | ● | ● | |||||
| MODIFIED | ● | ● | |||||
| OPEN_FILE | ● | ● | |||||
(See also Caveat above.)
⏹ Delete Operation
This operation closes the currently open equation and deletes the currently open file. This button can be enabled only if the open-equation and open-file properties are true. The modified property and save button can be in any state. Since an equation is open, the save as and close buttons, and the application frame subpanels must be enabled. After the operation is completed, the open-equation, open-file, and modified properties must be false; the save, save-as, close, and delete buttons and the application frame subpanels must be disabled.
| Delete Operation | |||||||
|---|---|---|---|---|---|---|---|
| Before | After | ||||||
| T | F | N | T | F | N | ||
| File/New | ● | ; | ● | ||||
| File/Open | ● | ● | |||||
| File/Save | ● | ● | |||||
| File/Save As | ● | ● | |||||
| File/Close | ● | ● | |||||
| File/Delete | ● | ● | |||||
| Panels | ● | ● | |||||
| OPEN_EQUATION | ● | ● | |||||
| MODIFIED | ● | ● | |||||
| OPEN_FILE | ● | ● | |||||
(See also Caveat above.)
Implementation Details
To complete the data management logic, we have new instance variables and a private method dedicated to each data management operation (new, open, save, etc.). We have new helper methods to support the operations, and the getFileMenu() method has to be updated. We’ll discuss these below.
⏹ Instance Variables
Our new instance variables are a reference to the enclosing CPFrame (if any) and a reference to the currently open file (if any): private final CPFrame cpFrame; private File currFile = null;
If there is no enclosing CPFrame, the value of the cpFrame variable will be null, and the data management operations will be effectively disabled. This variable is initialized in the CPMenuBar constructor, as shown below.
public CPMenuBar( Window topWindow )
{
// ...
if ( topWindow instanceof CPFrame )
cpFrame = (CPFrame)topWindow;
else
cpFrame = null;
}
⏹ Helper Methods
The following helper methods are designed to support the new data management methods.
🟦 private void loadEquation( Equation equation )
This method registers an equation with the enclosing CPFrame (if any) and configures the open-equation and modified properties. The modified property is always set to false. The value of the equation parameter determines the value of the open-equation property. Here’s the code.
private void loadEquation( Equation equation )
{
if ( cpFrame != null )
{
boolean hasData = equation != null;
cpFrame.loadEquation( equation );
pmgr.setProperty( CPConstants.DM_OPEN_EQUATION_PN, hasData );
pmgr.setProperty( CPConstants.DM_MODIFIED_PN, false );
}
}
🟦 private void setCurrFile( File file )
This method sets the value of the currFile instance variable and configures the open-file property accordingly. The code for this method follows.
private void setCurrFile( File file )
{
currFile = file;
boolean hasFile = file != null;
pmgr.setProperty( CPConstants.DM_OPEN_FILE_PN, hasFile );
}
⏹ Data Management Methods
Following is a discussion of the CPMenuBar methods dedicated to performing the data management operations.
🟦 private void newAction( ActionEvent evt )
This method is activated when the operator selects New from the file menu. It instantiates a new equation, registers the new equation with the CPFrame, and sets the current file to null. The code for it follows.
private void newAction( ActionEvent evt )
{
if ( cpFrame != null )
{
Equation equation = new Exp4jEquation();
loadEquation( equation );
setCurrFile( null );
}
}
🟦 private void openAction( ActionEvent evt )
This method is activated when the operator selects Open from the file menu. The FileManager class performs most of the work. Assuming that the operation succeeds and is not canceled, it updates the current file and the CPFrame. Here’s the code.
private void openAction( ActionEvent evt )
{
if ( cpFrame != null )
{
Equation equation = FileManager.open();
if ( equation != null )
{
setCurrFile( FileManager.getLastFile() );
loadEquation( equation );
}
}
}
🟦 private void saveAction( ActionEvent evt )
This method is activated when the operator selects Save from the file menu. Once again, the FileManager class performs most of the work. If the operation succeeds (there could be an I/O error), it updates the application state. The code follows below.
Note: Before executing the save operation, we ensure there is an open file. Of course, if there’s no open file, the operator shouldn’t even be able to initiate the operation, so there should be no need for the test. I think the test should be there, but we might consider throwing an exception if currFile is null.
private void saveAction( ActionEvent evt )
{
if ( cpFrame != null && currFile != null )
{
Equation equation = cpFrame.getEquation();
FileManager.save( currFile, equation );
if ( FileManager.getLastResult() )
{
setCurrFile( currFile );
pmgr.setProperty( CPConstants.DM_MODIFIED_PN, false );
}
}
}
🟦 private void saveAsAction( ActionEvent evt )
This method is activated when the operator selects Save As from the file menu. It initiates the FileManager operation that opens a FileChooser and asks the operator to select a target file. If the operation succeeds and is not canceled, it will update the current file and the application state. Here’s the code.
private void saveAsAction( ActionEvent evt )
{
if ( cpFrame != null )
{
Equation equation = cpFrame.getEquation();
FileManager.save( equation );
if ( FileManager.getLastResult() )
{
setCurrFile( FileManager.getLastFile() );
pmgr.setProperty( CPConstants.DM_MODIFIED_PN, false );
}
}
}
🟦 private void closeAction( ActionEvent evt )
The closeAction() method is activated when the operator selects Close from the file menu. It informs the enclosing CPFrame that the equation has been closed and updates the application state accordingly. Note that if a file is open, this action will close it (setCurrFile(null)). A listing of this method follows.
private void closeAction( ActionEvent evt )
{
if ( cpFrame != null )
{
cpFrame.loadEquation(null);
setCurrFile( null );
pmgr.setProperty( CPConstants.DM_MODIFIED_PN, false );
pmgr.setProperty( CPConstants.DM_OPEN_EQUATION_PN, false );
}
}
🟦 private void deleteAction( ActionEvent evt )
This method is activated when the operator selects Delete from the file menu. It changes the status of the currently open file and equation to closed, and it updates the enclosing CPFrame and the application status. Find the code below.
private void deleteAction( ActionEvent evt )
{
if ( currFile != null )
{
currFile.delete();
pmgr.setProperty( CPConstants.DM_MODIFIED_PN, false );
pmgr.setProperty( CPConstants.DM_OPEN_EQUATION_PN, false );
loadEquation( null );
setCurrFile( null );
}
}
⏹ private boolean asBoolean( PropertyChangeEvent evt )
This method works with a PropertyChangeEvent bound to one of the properties DM_OPEN_FILE_PN, DM_OPEN_EQUATION_PN, or DM_MODIFIED_PN. It interprets the new-value property of the PropertyChangeEvent as the String representation of a Boolean value (recall that the PropertyManager stores all values as type String so that a Boolean value will be, for example, “true” or “false”).
private boolean asBoolean( PropertyChangeEvent evt )
{
String newVal = evt.getNewValue().toString();
boolean result = Boolean.valueOf( newVal );
return result;
}
⏹ private JMenu getFileMenu()
The getFileMenu method configures the menubar’s File menu. We did most of the work for this method in the last lesson. As discussed above, we’ve added a few more menu buttons; see Refactoring. We have to link the menu buttons to their respective action methods. We’ll also add some logic to change the state of the buttons when the open-equation, open-file, and modified properties change. An annotated listing follows.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | private JMenu getFileMenu() { JMenu menu = new JMenu( "File" ); menu.setMnemonic( KeyEvent.VK_F ); JMenuItem newI = new JMenuItem( "New", KeyEvent.VK_N ); JMenuItem open = new JMenuItem( "Open", KeyEvent.VK_O ); JMenuItem save = new JMenuItem( "Save", KeyEvent.VK_S ); JMenuItem saveAs = new JMenuItem( "Save As", KeyEvent.VK_A ); JMenuItem close = new JMenuItem( "Close", KeyEvent.VK_C ); JMenuItem delete = new JMenuItem( "Delete", KeyEvent.VK_D ); JMenuItem exit = new JMenuItem( "Exit", KeyEvent.VK_X ); KeyStroke ctrlO = KeyStroke.getKeyStroke( KeyEvent.VK_O, ActionEvent.CTRL_MASK ); open.setAccelerator( ctrlO ); KeyStroke ctrlN = KeyStroke.getKeyStroke( KeyEvent.VK_N, ActionEvent.CTRL_MASK ); newI.setAccelerator( ctrlN ); KeyStroke ctrlS = KeyStroke.getKeyStroke( KeyEvent.VK_S, ActionEvent.CTRL_MASK ); save.setAccelerator( ctrlS ); newI.addActionListener( this::newAction ); open.addActionListener( this::openAction ); save.addActionListener( this::saveAction ); saveAs.addActionListener( this::saveAsAction ); close.addActionListener( this::closeAction ); delete.addActionListener( this::deleteAction ); exit.addActionListener( e -> System.exit( 0 ) ); menu.add( newI ); menu.add( open ); menu.add( save ); menu.add( saveAs ); menu.add( close ); menu.add( delete ); menu.add( exit ); Stream.of( save, saveAs, close, delete ) .forEach( b -> b.setEnabled( false ) ); pmgr.addPropertyChangeListener( CPConstants.DM_MODIFIED_PN, e -> save.setEnabled( asBoolean( e ) ) ); pmgr.addPropertyChangeListener( CPConstants.DM_OPEN_EQUATION_PN, e -> saveAs.setEnabled( asBoolean( e ) ) ); pmgr.addPropertyChangeListener( CPConstants.DM_OPEN_EQUATION_PN, e ->close.setEnabled( asBoolean( e ) ) ); pmgr.addPropertyChangeListener( CPConstants.DM_OPEN_FILE_PN, e ->delete.setEnabled( asBoolean( e ) ) ); return menu; } |
- Lines 3,4: Instantiate the File menu and set its mnemonic.
- Lines 6-12: Create the menu buttons and set their mnemonics.
- Lines 14-24: Set the accelerators on the Open, New, and Save buttons to ^O, ^N, and ^S, respectively.
- Lines 26-32: Add action listeners to the menu buttons.
- Lines 34-40: Add the menu buttons to the file menu.
- Lines 42,43: Set the initial value of the enabled property of the buttons that the operator can’t activate until the DM_OPEN_FILE_PN or DM_OPEN_EQUATION_PN are true.
- Lines 45-48: Add to the Save button a property-change listener that will configure the button’s enabled property when the modified property changes.
- Lines 50-53: Add a property-change listener to the Save As button that will configure the button’s enabled property when the open-file property changes.
- Lines 55-58: This adds a property-change listener to the Close button, configuring the button’s enabled property when the open-equation property changes.
- Lines 60-63: Add a property-change listener to the Delete button, configuring the button’s enabled property when the open-file property changes.
- Line 65: Return the File menu.
Summary
On this page, we completed the development of the data management features of the application menubar, CPMenuBar. Implementing these features required us to implement methods to invoke when the New, Open, Save, Save As, Close, and Delete buttons are activated. We also had to add property-change listeners to the Save, Save As, Close, and Delete buttons that configured their enabled property when one of the DM_OPEN_FILE_PN, DM_OPEN_EQUATION_PN, or DM_MODIFIED_PN changed. On the next page, we will develop a test for those features. We’ll also examine another feature of the JUnit test library: test suites.