The UISystem.java file contains the code for the singleton instance – the object instance used to make all the API calls. A private constructor ensures that users cannot instantiate additional instances via the new operator. The static getInstance() method must be used to obtain the singleton instance:
public final class UISystem { private static final UISystem INSTANCE = new UISystem(); ... private UISystem() { } public static UISystem getInstance() { return INSTANCE; }
The versatile JOptionPane
The first four status modal dialog methods are actually thin wrappers around the static methods provided by the JOptionPane in the swing library. The JOptionPane class is a class for displaying different styles of managed dialog boxes.JustNuff Method | JOptionPane Method |
showInformationModalDialog | showMessageDialog |
showWarningModalDialog | showMessageDialog |
showErrorModalDialog | showMessageDialog |
showYesNoCancelModalDialog | showConfirmDialog |
The first three methods above are all slightly different parameterisation of the option pane's showMessageDialog() method. For example, the showErrorModalDialog() method has the following code:
public void showErrorModalDialog( String inText) throws Exception { final String tpText = inText; Runnable runSeg = new Runnable() { public void run() { JOptionPane.showMessageDialog( null, tpText, "Error", JOptionPane.ERROR_MESSAGE); } }; EventQueue.invokeAndWait(runSeg); }The first null parameter indicates that this modal dialog has no specified parent GUI element. The swing library will then create a shared invisible frame to act as its parent. Invocation of this static method is quite efficient since the swing library manages the instances of dialog objects that are created, and will reuse instances.
Note the use of an anonymous Runnable class to bracket the call into the swing library. This is the mechanism we have discussed earlier to synchronise between the application logic and the GUI subsystem.
Making thread-safe Swing calls
Because Swing is not a thread-safe library, it is very important that we ensure that the GUI manipulations that we perform are done via the one and only GUI event handling thread. By creating a Runnable anonymous class that implements our actions, the swing API calls that we make can be turned into an event for the GUI event handling thread to execute. When we call the invokeAndWait method from the event queue, we will block execution until this body of code (that is everything inside the run() method of the Runnable interface) is executed. This will ensure the actions are performed by the event handling thread, thus honouring the thread-safety requirement of the Swing library. You will see this construct throughout the JustNuff library. This construct ensures that we satisfy Requirement 2 as listed in last month's article, that is:- All library APIs must be callable from an application logic thread
Figure 1
Figure 1 illustrates the interaction flow. It should be clear that all GUI work will be confined to the GUI handler thread, and that the JustNuff API calls can be made at any time from the application logic thread without thread-safety problems.
Using JOptionPane to obtain input
To create the showTextInputModalDialog() method, we can wrap JOptionPane's static showInputDialog() method:private String strRetVal = null; public String showTextInputModalDialog(String inText) throws Exception { final String tpText = inText; Runnable runSeg = new Runnable() { public void run() { strRetVal = JOptionPane.showInputDialog( null, tpText); } }; EventQueue.invokeAndWait(runSeg); return strRetVal; }Note that the shared variable here, strRetVal, is assigned a value by the GUI handler thread while the application thread is blocked. After the GUI handler thread completes its operation, the application thread is unblocked and strRetVal is then accessed from the application logic thread and returned to the caller.
Creating custom dialogs
While the static methods in JOptionPane handle the display and management of status message boxes and basic text string input well, they fall short if we need any additional customisation and functionality. If we need an input dialog for (say) a phone number or a date, then we are left to our own devices to come up with a solution.The easiest, and compatible, way to accomplish this is via the composition of JOptionPane. To show how we can create our own custom modal dialogs by embedding an instance of JOptionPane, the JustNuff library contains showNumericInputModalDialog that return a long typed value. This method uses a custom NumericInputModalDialog class. Figure 2 illustrates the construction of this custom NumericInputModalDialog class.
Figure 2
In our code, we need to perform the following steps:
- subclass JDialog
- set the parent to behave in a modal manner
- create a custom text field that only return valid long typed value
- create a customised instance of JOptionPane
- set the content pane of the JDialog with the customised JOptionPane instance, enabling the content pane to control the appearance and interaction of the dialog
- handle the property change event from button click
- transfer the data from the custom text field to the return value when OK is clicked
- hide the dialog (for reuse!) and clear the text field if CANCEL is clicked or if the box is closed
- provide custom cleanup logic when the dialog is closed
- provide public accessor methods to the prompt message, and the return value
package uk.co.vsj.ui.justnuff; import javax.swing.JDialog; import javax.swing.JOptionPane; import javax.swing.JLabel; import javax.swing.JFormattedTextField; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeEvent; import java.awt.Frame; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.text.NumberFormat; 1final class NumericInputDialog extends JDialog implements PropertyChangeListener { private long numericValue = 0; private JFormattedTextField textField; private JOptionPane optionPane; private String okButtonText = "OK"; private String cancelButtonText = "Cancel"; private NumberFormat numberFormat = NumberFormat.getNumberInstance(); private JLabel messageLabel; public NumericInputDialog(Frame parentFrame, String promptMsg ) { 2 super(parentFrame, true); setTitle("Please enter a number..."); messageLabel = new JLabel(promptMsg); 3 textField = new JFormattedTextField(numberFormat); Object[] array = {messageLabel, "", textField}; Object[] options = {okButtonText, cancelButtonText}; //Create the JOptionPane. 4 optionPane = new JOptionPane(array, JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_NO_OPTION, null, options, options[0]); 5 setContentPane(optionPane); setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent we) { optionPane.setValue(new Integer( JOptionPane.CLOSED_OPTION)); } }); 6 optionPane.addPropertyChangeListener(this); } 6 public void propertyChange(PropertyChangeEvent e) { String prop = e.getPropertyName(); if (isVisible() && (e.getSource() == optionPane) && (JOptionPane.VALUE_PROPERTY.equals(prop) || JOptionPane.INPUT_VALUE_PROPERTY.equals(prop))) { Object value = optionPane.getValue(); if (value == JOptionPane.UNINITIALIZED_VALUE) { //ignore reset return; } optionPane.setValue( JOptionPane.UNINITIALIZED_VALUE); if (okButtonText.equals(value)) { 7 try { numericValue = ((Long) textField.getValue()).longValue(); } catch (Exception ex) { // should never happen ex.printStackTrace(); numericValue =0; } clearAndHide(); } else { //user closed dialog or clicked cancel 8 numericValue = 0; clearAndHide(); } } } 9 private void clearAndHide() { textField.setText(null); setVisible(false); } 10 public void setPromptMessage(String inText) { messageLabel.setText(inText); } 10 public void setNumericValue(long inVal) { numericValue = inVal; textField.setText(String.valueOf(inVal)); textField.selectAll(); } 10 public long getNumericValue() { return numericValue; } }
Meeting design requirements
One of our design requirements is to minimise the number of objects created, another is that instances of complex dialog should be reused. The showNumericInputModalDialog() achieves this by reusing a singleton static instance of a NumericInputModalDialog(). The following code segment illustrates how this is done:private static final NumericInputDialog numDlg = new NumericInputDialog(null, ""); ... private long longRetVal = 0; public long showNumericInputModalDialog(String inText) throws Exception { final String tpText = inText; Runnable runSeg = new Runnable() { public void run() { numDlg.setPromptMessage(tpText); numDlg.setNumericValue(0); numDlg.pack(); numDlg.setVisible(true); longRetVal = numDlg.getNumericValue(); } }; EventQueue.invokeAndWait(runSeg); return longRetVal; }If you examine the code of showDateInputModalDialog() and showPhoneInputModalDialog(), you will see the same technique used to reuse the singleton static instances of DateInputModalDialog and PhoneInputModalDialog respectively.
Refactoring
Another design requirement is to ensure that the library can be easily extended. Since we cannot possibly provide enough customised dialogs for every user of the library, it is a good idea to make it simpler to create new customised dialog classes. The code of NumericInputDialog is quite intricate, and can be error prone because it combines both dialog handling code and field and data management code. Since every custom dialog has the same pair of buttons, has some fields for users to enter data, and has to transfer data into the return value when the "OK" button is pressed, it is possible to refactor the logic of NumericInputDialog and create a base class for all custom dialogs. This class is called CustomInputDialog in the JustNuff library, and it is declared abstract since it is designed to be extended and not instantiated directly. The listing below is the source from NumericInputDialog, annotated with the steps that we saw in the NumericInputDialog custom logic. Note that step 3 is missing, steps 7–9 are calls on abstract functions, and step 10 is also partially missing. These are the precise steps that will be supplied by the subclass of CustomInputDialog since they differ for each custom dialog.package uk.co.vsj.ui.justnuff; import javax.swing.JOptionPane; import javax.swing.JDialog; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeEvent; import java.awt.Frame; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; 1public abstract class CustomInputDialog extends JDialog implements PropertyChangeListener { private String typedText = null; private JOptionPane optionPane; private String okButtonText = "OK"; private String cancelButtonText = "Cancel"; protected CustomInputDialog(Frame parentFrame) { 2 super(parentFrame, true); } 4 protected void createPane(Object [] array) { //Create the JOptionPane. Object[] options = {okButtonText, cancelButtonText}; optionPane = new JOptionPane(array, JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_NO_OPTION, null, options, options[0]); //Make this dialog display it. 5 setContentPane(optionPane); setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent we) { optionPane.setValue(new Integer( JOptionPane.CLOSED_OPTION)); } }); 6 optionPane.addPropertyChangeListener(this); } 6 public void propertyChange(PropertyChangeEvent e) { String prop = e.getPropertyName(); if (isVisible() && (e.getSource() == optionPane) && (JOptionPane.VALUE_PROPERTY.equals(prop) || JOptionPane.INPUT_VALUE_PROPERTY.equals(prop))) { Object value = optionPane.getValue(); if (value == JOptionPane.UNINITIALIZED_VALUE) { //ignore reset return; } optionPane.setValue( JOptionPane.UNINITIALIZED_VALUE); if (okButtonText.equals(value)) { 7 afterOKButtonClick(); clearAndHide(); } else { //user closed dialog or clicked cancel 8 afterCancelButtonClick(); clearAndHide(); } } } 9 protected void clearAndHide() { beforeClosing(); setVisible(false); } protected abstract void afterOKButtonClick(); protected abstract void afterCancelButtonClick(); protected abstract void beforeClosing(); 10 public abstract void setPromptMessage(String inText); }Subclassing from CustomInputDialog can make extending the JustNuff library quite simple. This is how we constructed DateInputModalDialog (for input of date typed data) and PhoneInputModalDialog (for input of phone number typed data), and how you can create your own specialised input dialog class.
Using the JDK 1.4 GUI
One of the requirements of our JustNuff library was to use some of the new GUI features in JDK 1.4, in order to give a current and contemporary feel to the GUI. The fact that one of these new features actually simplifies the coding of the library is a big plus. The JDK 1.4 enhancement that we will exploit is JFormattedTextField. JFormattedTextField is a subclass of JTextField, and it provides the ability to format data displayed in the field and/or filter data being input into the field.A complete detailed discussion of the MVC (model-controller-view) architecture used by JFormattedTextField is beyond the scope of this article. Interested readers are encouraged to go through the JavaDoc documentation and tutorial supplied with JDK 1.4. For our purposes, it is adequate to know that one can specify the data formatter to be used by JFormattedTextField when the field is instantiated. More concretely, the following code can be used to obtain input into a date typed data field:
private SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy"); ... textField = new JFormattedTextField(dateFormat );The above code will return the input data in month/day/year format, as specified by the instance of SimpleDateFormat. As of JDK 1.4, however, it will not restrict input to the specific date format.
Also the following code can be used to obtain a phone-formatted value from a JFormattedTextField:
MaskFormatter mformat = null; try { mformat = new MaskFormatter("(###) ###-####"); phoneField = new JFormattedTextField(mformat); } catch (java.text.ParseException ex) { ... }A MaskFormatter instance can be used to filter and explicitly restrict the input of data according to a specified mask. Some of the valid characters in the mask specification are tabulated below.
Mask char | Purpose |
# | Any digit 0–9 |
U | Any letter, input converted to upper case |
L | Any letter, input converted to lower case |
A | Any letter or digit |
? | Any character, no case conversion |
* | Anything – unrestricted character |
H | Any hex digit |
' | Escape any mask character |
Other chars | Reproduce the character exactly |
In the NumericInputDialog, we have already seen the code that obtains input of a numeric typed data:
private NumberFormat numberFormat = NumberFormat.getNumberInstance(); ... textField = new JFormattedTextField(numberFormat);
Creating new dialog types
Let's take a look, then, at how the PhoneInputDialog takes advantage of the CustomInputDialog abstract class as well as the JDK 1.4 JFormattedTextField. The code of PhoneInputDialog is presented below, and the corresponding steps (from the earlier dialog list) are labelled. Compare this to the coding of NumericInputDialog and you will appreciate how the CustomInputDialog abstract class saves significant work when creating new custom dialogs.package uk.co.vsj.ui.justnuff; import java.util.Date; import javax.swing.JFormattedTextField; import javax.swing.JLabel; import java.awt.Frame; import java.text.SimpleDateFormat; final class DateInputDialog extends CustomInputDialog { private Date dateValue = null; private JFormattedTextField textField; private JLabel messageLabel; private SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy"); public DateInputDialog(Frame parentFrame, String promptMsg ) { super(parentFrame); messageLabel = new JLabel(promptMsg); System.err.println("in constructor"); setTitle("Please enter date..."); 3 textField = new JFormattedTextField(dateFormat ); textField.setValue(new Date()); Object[] array = {messageLabel, "", textField}; createPane(array); } 7 protected void afterOKButtonClick() { try { dateValue = dateFormat.parse(textField.getText()); } catch (Exception ex) { dateValue = null; } } 8 protected void afterCancelButtonClick() { System.err.println("other branch entered..." ); dateValue = null; } 9 protected void beforeClosing() { textField.setValue(null); } 10 public void setPromptMessage(String inText) { messageLabel.setText(inText); } 10 public void setInputDate(Date inDate) { dateValue = inDate; textField.setText(dateFormat.format(dateValue)); textField.selectAll(); } 10 public Date getInputDate() { return dateValue; } }Of course, the JustNuff library's UISystem class manages the singleton DateInputDialog used. This code is similar to that of the showNumericInputModalDialog() method:
private static final DateInputDialog dateDlg = new DateInputDialog( null, ""); ... private Date dateRetVal; public Date showDateInputModalDialog( String inText) throws Exception { final String tpText = inText; Runnable shownDialog = new Runnable() { public void run() { dateDlg.setPromptMessage(tpText); dateDlg.setInputDate(new Date()); dateDlg.pack(); dateDlg.setVisible(true); dateRetVal = dateDlg.getInputDate(); } }; EventQueue.invokeAndWait(shownDialog); return dateRetVal; }
Handling object input
An ObjectInputDialog will take an object as input and generate on-the-fly a dialog that can be used to obtain values for each public field of the object. This is a versatile feature that is implemented using the reflection API. The reflection API enables us to discover the details of any class at runtime.Here is how ObjectInputDialog does its work. First, there is a helper class called FieldWrapper. This class associates the field name with the GUI text field that contains its value. It also has two helper methods, one to transfer data from the text field into the object, and the other one for clearing up the object's field value:
class FieldWrapper { JFormattedTextField _intField; String _fieldName; public FieldWrapper(String inName, JFormattedTextField inField) { _fieldName = inName; _intField = inField; }xferData() moves data from the GUI text field to the object. Note the use of Field.set() method that works as long as the type of the second calling argument is the same as the type of the field:
public void xferData(Object inObj) { Class objClass = inObj.getClass(); Field curField = null; try { curField = objClass.getField( _fieldName); } catch (NoSuchFieldException ex) { // should never occur ex.printStackTrace(System.err); curField = null; // force assert } assert curField != null; Object curValue = _intField.getValue(); if (curValue != null) { try { curField.set(inObj, _intField.getValue() ); } catch (IllegalAccessException ex) { // should never happen because we // already accessed fields earlier ex.printStackTrace(System.err); } } }The zapData() method cleans up the value of the GUI field:
public void zapData() { _intField.setValue(null); } }ObjectInputDialog extends CustomInputDialog to simplify coding:
class ObjectInputDialog extends CustomInputDialog { private String typedText = null; private JFormattedTextField textField; private JLabel messageLabel;The fieldsArray will be used to hold the object's public data field to GUI field mapping information, there will be one entry for each public data field in the object. The object itself is stored in the inputObject private variable:
private FieldWrapper [] fieldsArray; private Object inputObject = null; public ObjectInputDialog(Frame parentFrame, String promptMsg , Object inObj) { super(parentFrame); inputObject = inObj; messageLabel = new JLabel(promptMsg); System.err.println("in ObjectInputDialog constructor for class " + inObj.getClass().getName()); setTitle("Please Enter Data...");Consider the incoming object. First we get its class, then use the class to get the fields:
Class objClass = inObj.getClass(); Field [] objFields = objClass.getFields(); int fieldsCount = objFields.length;Next, we create the fieldsArray and an object array that will be used to create the GUI. More specifically, the array for the GUI will contain alternating labels (field names) and JFormattedText elements. This is why it is twice the size of fieldsArray:
Object [] array = new Object[fieldsCount * 2]; fieldsArray = new FieldWrapper[fieldsCount];For simplicity, we will only handle objects with five or less public data fields:
try { if (fieldsCount > 5) { // more fields than we can handle, // revert to simple information dialog throw new Exception( "too many fields to process"); }Next, we get the name of the field, and the name of the type of the field, using the type to decide how to instantiate the JFormattedTextField element. This operation combines the JFormattedTextField usage techniques from the other data type specific dialogs we have created earlier:
String fieldName = objFields[i].getName(); String fieldType = objFields[i].getType().getName(); if (fieldType.equals("int") || fieldType.equals("long")) { array[2 * i] = fieldName; array[2 * i+1] = new JFormattedTextField( NumberFormat.getIntegerInstance()); fieldsArray[i] = new FieldWrapper( fieldName, (JFormattedTextField) array[2 * i + 1]); } else if (fieldType.equals( "java.lang.String") && fieldName.endsWith("P")) { // this is a phone number field, a // specialised string field - you can // have many more here array[2 * i] = fieldName.substring( 0,fieldName.length() -1); array[2 * i+1] = new JFormattedTextField(new MaskFormatter("(###) ###-####")); fieldsArray[i] = new FieldWrapper(fieldName, (JFormattedTextField) array[2 * i + 1]); } else if (fieldType.equals( "java.lang.String")) { // this is a regular string field array[2* i] = fieldName; array[2* i+1] = new JFormattedTextField(new String("")); fieldsArray[i] = new FieldWrapper( fieldName, (JFormattedTextField) array[2 * i + 1]); } else { // at least one field we cannot // process, reverting to simple // information dialog throw new Exception( "field type not supported exception"); } } // of for loop } catch (Exception ex) {If the object has more than five fields, or if there is at least one field that we cannot handle, then we create a error status dialog box instead and print a stack trace:
ex.printStackTrace(System.err); fieldsArray = null; array = new Object[1]; array[0] = "Sorry, class cannot be handled by ObjectInputDialog"; } createPane(array); }If the user clicks the OK button, we go through the fieldsArray and transfer the data from the GUI field into the object's data field:
protected void afterOKButtonClick() { if (fieldsArray != null) for (int i=0, n=fieldsArray.length; i<n; i++) { fieldsArray[i].xferData(inputObject); } } protected void afterCancelButtonClick() { // no need to do anything }Before the dialog is hidden, we go through the fieldsArray and clear out all the JFormattedTextField elements via the zapData() method. This will ensure that the fields will all be null when the dialog is reused:
protected void beforeClosing() { // need to reset all the textfields if (fieldsArray != null) for (int i=0, n= fieldsArray.length; i<n; i++) { fieldsArray[i].zapData(); } }The last three methods are public accessors to the prompt text and the input object.
public void setPromptMessage(String inText) { messageLabel.setText(inText); } public Object getInputObject() { return inputObject; } public void setInputObject(Object inObj) { inputObject = inObj; } }Unlike other custom dialogs such as DateInputDialog, each object type that needs to input requires its own dialog instance. For example, we need one custom dialog to input the BankAccount class, and we need one custom dialog instance for the input of the BankAccount2 class. On the other hand, if we show the dialog for inputting a BankAccount class multiple times, then we don't want JustNuff to create a new dialog instance each time the showObjectInputModalDialog() method is called. The best way to accomplish this is to create a map that tracks the dialog instances (that are already created) and maps them to the corresponding object class that they handle. See the code fragment below:
private static final Map dialogStore = new HashMap(); ... Object objectRetVal = null; public Object showObjectInputModalDialog(String inText, Object inObj) throws Exception { final ObjectInputDialog curDialog; Class tpClass = inObj.getClass(); if (dialogStore.containsKey( tpClass)) curDialog = (ObjectInputDialog) dialogStore.get(tpClass); else { // create and add a new object // dialog for the class curDialog = new ObjectInputDialog(null,"", inObj); dialogStore.put(tpClass, curDialog); } ...The keys to the dialogStore HashMap are the classes of the object, while the values are the dialogs that are created to handle each corresponding class. This approach makes sure that we only create a single instance of a custom dialog for each class that we can handle.
Conclusion
It is somewhat ironic, yet predictable, that the creation of an easy-to-use "just enough" GUI library in Java requires an almost expert level of knowledge of the Swing GUI library. To alleviate the need for the user to handle tricky thread-safety problems, we handle it in the library. To ensure that GUI objects are allocated efficiently, we have programmed in object reuse via the static singleton coding pattern. To deploy the latest JDK 1.4 GUI elements, we've made extensive use of the new JFormattedTextField, including masked input. To provide ease-of-use when inputting multiple fields in an object, we've used the reflection API to dynamically discover the composition of the object during runtime. Last but not least, by re-factoring the relatively complex logic used to subclass JDialog when creating a custom dialog, we have enabled the JustNuff library to be extended easily to support additional data type specific or other custom dialogs. This behind-the-scenes look at the JustNuff GUI library offers plenty of food for thought for library builders and GUI developers alike.Sing Li is a consultant, trainer and freelance writer specialising in Java, web applications, distributed computing and peer-to-peer technologies. His recent publications include Early Adopter JXTA, Professional JINI and Professional Apache Tomcat, all published by Wrox Press.
Comments