User interfaces for object-oriented systems, Part 6: The RPN calculator

The RPN-calculator application demonstrates object-oriented UI principles

This month we continue the process begun in the January Java Toolbox by completing the RPN (Reverse Polish Notation) calculator application. The RPN calculator is a small but nontrivial application that demonstrates some of the important object-oriented UI principles we've looked at in previous Java Toolbox articles, specifically the visual-proxy design pattern. This application isn't just a toy; I use it virtually every day -- every time I need a calculator, in fact.

A word of warning: the following discussion will be incomprehensible if you haven't read the earlier installments of this series.

TEXTBOX: TEXTBOX_HEAD: Build user interfaces for object-oriented systems: Read the whole series!

Reverse Polish Notation was championed for years by Hewlett-Packard, though I've been told that their most recent calculators don't support it anymore (a pity). It is one of those things that are difficult to learn but wonderful to use once you understand them. The RPN's basic notion is built around an arithmetic stack. Numbers, when entered, are pushed on the stack, and all operations use stack items as operands.

For example, when you press the add (+) key, the two items closest to the top of the stack are popped and added together, and the resulting sum is pushed, effectively replacing the original operands. Although that might seem like a strange way to do things, you never need to use parentheses, and once you get used to it, you'll probably rather like it. I've been using my PalmPilot as a handheld calculator, using Russ Webb's great RPN calculator (see Resources), but I wanted one for my computer too. Being a programmer, I thought building one was the easiest way to get exactly what I wanted.

The analysis model

Since the structure of the calculator's UI is closely related to the object model, let's start by looking at the analysis-level static model, shown in Figure 1.

As is the case with all object-oriented designs, the analysis-level classes are found in the implementation as well. Figure 1, therefore, is really an implementation-level diagram of the original analysis diagram. In object-oriented systems, the design-level model is nothing but the analysis-level model with implementation-level detail added.

Figure 1. The analysis-level static model

The first analysis-level class of interest -- Rpn -- contains the main() method and little else. Though they aren't shown in the diagram, the class also contains a constructor and implementations of all the methods required by the interfaces. In an attempt to reduce the clutter a bit, I usually don't show these in a Unified Model Language (UML) diagram. As is the case with many object-oriented programs, main() (and the Rpn class, for that matter) aren't much to look at. Object-oriented systems tend to be networks of cooperating objects, with no central God class that controls everything from above. There's no spider sitting in the middle of the web pulling strands. An object-oriented system's main() method, as a consequence, typically creates a few objects, hooks them up to each other, and then terminates. That's exactly what happens here: main() creates an instance of the Parser and Math_stack, hooks them up to each other, and terminates.

The main reason that I have an Rpn class at all is that I need an implementation-level class to take care of creating the main frame. Rpn is not a God class, however. Though it continues to exist after the program launches, it is not an active participant in the program.

The other two analysis-level classes constitute the actual calculator: the Parser parses the user's input and passes it to the Math_stack, which does the actual arithmetic. We'll look in depth at both of those classes in a moment, but for now note that the Math_stack contains methods like push(), pop(), and add(). The Parser, which contains no analysis-level methods of interest, receives the user's input and sends appropriate requests to the Math_stack. Therefore, no analysis-level messages are sent to it.

Visual proxies in action

The calculator's UI follows the visual-proxy architecture that I discussed in the first couple of installments of this series. Using the vocabulary of the previous articles, the Rpn class is a control object, while the Math_stack and Parser objects make up the abstraction layer.

The control object assembles a presentation by asking the abstraction-layer classes for visual proxies -- JComponents that represent the object's state. Both the Math_stack and Parser implement User_interface, so they can produce visual proxies when asked. The main purpose of the Rpn class, with respect to the UI, is to ask the Parser and Math_stack objects for those visual proxies, which it then lays out and displays. The Rpn object itself creates the main frame and implements a Menu_site that manages the menu bar.

Note: The proxies can get a reference to the encapsulating container's Menu_site component by calling

(Menu_site)( SwingUtilities.getAncestorOfClass( Menu_site.class, myself ) );

which returns a reference to the object above myself in the runtime window hierarchy that implements Menu_site. That is, it returns a reference to some container of the myself object, provided that the container implements or extends the indicated interface or class.

So, when it's created, the Rpn object asks the stack and parser for visual proxies and then displays them. The visual proxies don't speak to the Rpn object as such, but they will interact with it through the Menu_site interface when they need to customize the menu bar. The proxies think of Rpn exclusively as a Menu_site -- they don't know the actual class name.

It's important to note the decoupling of these UI elements. The Math_stack's proxy doesn't know that the Parser's proxy exists, much less what it looks like -- and vice versa. Similarly, the Rpn object treats the proxies as simple JComponents (it positions them within the frame but does absolutely nothing else with them), while the proxies treat the Rpn object as a Menu_site.

Moreover, the main frame is simply a passive vehicle for holding visual proxies. The proxies communicate directly with the analysis-level object that creates them -- the frame isn't involved -- and these analysis-level objects can communicate with each other. If the state of an analysis-level class changes as a result of some user input, it sends a message to another of the abstraction-level classes, which may or may not choose to update its own UI (its proxy) as a consequence.

Figure 2 shows the resulting UI. The Rpn class owns (creates and manages) the main frame and menu bar. The top, gray window, which displays the current contents of the Math_stack object's arithmetic stack, is the Math_stack's visual proxy. The yellow and white windows are the Parser object's UI. (The Parser's visual proxy is a single JPanel that contains the other widgets.) When you type into the bottom input window, the Parser parses what you type, displays a record of your requests in the yellow "tape" window (which acts more or less like the paper tape on a financial calculator), and sends appropriate arithmetic requests (push, pop, add, and so on) to the Math_stack object. The Math_stack does the work, then updates its proxy to display the new state of the stack. There's no UI-related messaging between analysis-level objects. The Parser sends domain messages (push, pop, and so on) to the Math_stack. The fact that the Math_stack updates its UI as a consequence of receiving these messages is irrelevant.

Figure 2. The initial UI

As it turns out, the Parser's visual proxy can display itself in one of two ways: the Good way, which uses the computer's keypad, and the Bad way, which simulates a keyboard on the screen. When the proxy is created, it negotiates with the control object and adds the Interface item to the main menu bar. That is, the Rpn object adds the File and Help menus, but the visual proxy adds the Interface menu for the Parser. Clicking on this menu shows you the two choices, as seen in Figure 3.

Figure 3. The interface menu

The Good interface is the initial choice, while selecting Bad causes the Parser object's proxy to change its appearance to that shown in Figure 4.

Figure 4. The Bad interface

I think of the second choice as the Bad interface because, from the perspective of usability, the notion that a virtual calculator should look like a physical one is an absurdity. Computer keyboards have perfectly good numeric keypads on them, and there's no reason to simulate that keypad onscreen. I read a paper a while back that claimed that a significant number of people who used the Windows-provided calculator didn't know that they could use the keyboard to control it; they assumed that since the UI displayed buttons, that they had to use those buttons. I've seen the notion of duplicating physical interfaces taken to absurd places. For example, one HP calculator emulator was so true to the original that each button had three purposes. You had to select the secondary and tertiary purposes by clicking on a blue or yellow function key.

This slavish mimicking of the real world does make the program easy to learn, but don't confuse ease of learning with ease of use. I suppose the one saving grace of the Bad interface is that you can use it when no keyboard is available as in a PDA. But that's a special case; the UI should normally be hidden.

You'll note that when the Parser's proxy changed its appearance, it also added an Advanced menu to the menu bar, as seen in Figure 5. This menu gives you access to the calculator functionality for which there's no keyboard button.

Figure 5. The Advanced menu

By the same token, you'll notice that the Advanced menu goes away if you switch back to the Good interface and that the Help menu now includes a user-interface item. (I haven't shown it in a figure, so you'll have to use your imagination.) This menu pops up the window in Figure 6, which shows you what you can type from the keyboard. Again, the proxies do this work. The Rpn object hosts the menu site, but it's a passive participant in the menu negotiation. The proxy for the Parser object effectively communicates directly to the Menu_site.Implementation and adds whatever menu items that it needs.

To summarize, the menu bar in the visual-proxy architecture is a composite of menu items created by the individual proxies. When those menu items are selected, the resulting notification goes directly to the proxy that created the menu item. The Menu_site implementation has no involvement.

Figure 6. The user-interface Help menu Click on thumbnail for full image (20k)

The Rpn class implementation

Since we're here, we may as well look at Rpn's implementation (in Listing 1) before going on to the detailed implementation model. As you saw in Figure 1 above, the Rpn class has four fields of interest. The math_engine (Listing 1, line 45) and the parser (Listing 1, line 46) reference the Math_stack and Parser objects that compose the logical model. The parser_viewer field (Listing 1, line 47: a JComponent reference) points at the parser's visual proxy. The stack_viewer reference to the Math_stack's visual proxy (shown as a dashed line in Figure 1) is a local variable of the constructor. (That's why the line is dashed: stack_viewer isn't a field, but there is a relationship between the two classes.)

As I mentioned earlier, main(...) (Listing 1, line 76) does little or nothing. It instantiates an instance of Rpn() and prints a help message if someone launches the program with the wrong number of arguments. Most of the work is done in the constructor, Rpn() (Listing 1, line 102). The constructor creates the system-level menus (File and Help) and installs them on the Menu_site.Implementation object. It then gets the two visual proxies from the abstraction layer (Listing 1, line 142) and defines the component-level interaction between the proxies. (When the Math_stack proxy gets focus, the focus is transferred to the Parser proxy.) The Rpn object then creates a splitter frame and installs the proxies into it. Finally, the Rpn object installs the splitter into main frame. Once the UI is set up, the constructor -- and main() -- terminate.

From this point on, the program is really a small network of the two objects that make up the abstraction layer, which communicate with each other. Rpn has no involvement beyond providing a host site for the menu bar. Put another way, the UI and the logical model -- the abstraction layer -- are quite decoupled. You can radically change the organization of the UI without changing the abstraction-layer classes at all, and you can radically change the abstraction-layer classes without affecting the UI-layout code. Moreover, if you do change an abstraction-layer class, all the UI changes (the parts of the UI that expose the state of the class) are concentrated in one place. This UI change will affect the appearance of the whole program, however.

Let's now move on to the Menu_site. The Menu_site interface contains an inner-class implementation called Menu_site.Implementation. Normally, a class could be a menu site simply by extending this inner class, but that's not an option here because our one and only extends relationship is already used up by the JFrame base class. Consequently, the Rpn class (Listing 1) implements the Menu_site interface's methods as simple pass-throughs, which do nothing but chain through to the equivalently named methods of the Menu_site_support object: that is, support; see Listing 1, line 49). These pass-through methods are at the very end of Listing 1.

This way of doing things -- implementing an interface with pass-through methods that chain to methods of an implementation class -- is a common enough idiom that it ought to be an official design pattern, but as far as I know, it's as yet unnamed. You can use this mechanism to implement true multiple inheritance in Java: from the perspective of a user of Rpn objects, Menu_site effectively acts not as an interface but as a base class that contains an implementation. Rpn really uses a paired interface/implementation class, however. This design pattern, then, lets you implement multiple inheritance with all the flexibility of C++, for example, without any of the potential ambiguity problems implicit in C++'s inheritance mechanism.

Listing 1 (/src/rpn/Rpn.java): Implementation of the analysis-level model
   1: package rpn;
   2: 
   3: import java.awt.*;
   4: import javax.swing.*;
   5: import java.awt.event.*;
   6: 
   7: import com.holub.ui.Menu_site;
   8: import com.holub.ui.Scrollable_JTextArea;
   9: import com.holub.tools.debug.Assert;
  10: 
  11: /** © 2000, Allen I. Holub. All rights reserved.
  12:  *
  13:  * Rpn.java application is a simple RPN calculator. Its UI
  14:  * consists of two windows, one showing the stack and another showing
  15:  * the current input string. Type an RPN expression into the input
  16:  * window and the results appear in the stack window. Type "?" to
  17:  * display a help window. A third window displays a financial-style
  18:  * "tape" that shows a history of what you've done.
  19:  *
  20:  * This application nicely demonstrates the visual-proxy
  21:  * architecture. The presentation layer is created from visual
  22:  * proxies provided by abstraction-layer objects (parser and stack).
  23:  * Each of these objects is responsible for updating its own UI.
  24:  * The Parser object doesn't even know that the stack
  25:  * displays a user interface, much less what the presentation
  26:  * looks like; rather, it manipulates the "abstraction" directly and
  27:  * the "abstraction" takes care of its own UI. All proxies are
  28:  * declared "JComponent"s at this level, so the details of how
  29:  * they work really are hidden.
  30:  *
  31:  * The current application is easily turned into an applet by
  32:  * extending java.awt.Applet rather than java.awt.Frame, and
  33:  * overriding the appropriate applet methods. You will need to get
  34:  * rid of the System.exit() call in the WindowAdapter created by the
  35:  * constructor, however.
  36:  *
  37:  * @author  Allen I. Holub
  38:  * @version 2.01
  39:  */
  40: 
  41: 
  42: public class Rpn extends    JFrame
  43:                             implements  Menu_site
  44: {
  45:   private final Math_stack math_engine   = new Math_stack( 512    );
  46:   private final Parser     parser        = new Parser  ( math_engine );
  47:   private       JComponent parser_viewer; // = null
  48: 
  49:   private final Menu_site.Implementation support
  50:                                 = new Menu_site.Implementation(this);
  51: 
  52:   private static final String[] about_message =
  53:     {
  54:     "      RPN Calculator ver. 2.01. © 2000 Allen I. Holub.",
  55:     "",
  56:     "This application was downloaded from Allen Holub's Website:",
  57:     "",
  58:     "                http://www.holub.com",
  59:     "",
  60:     "where you'll find information about Java and-object-oriented training",
  61:     "and other Java-related goodies. The .class files that",
  62:     "comprise this application may be distributed freely for",
  63:     "noncommercial purposes, provided that they are distributed",
  64:     "without modification.",
  65:     "",
  66:     "The source code for this application may not be redistributed.",
  67:     "Period.",
  68:     "",
  69:     "Usage: java [-Dlog.file=log_file_name] rpn.Rpn"
  70:     };
  71: 
  72:     /************************************************************
  73:      * Creates an RPN calculator. If any command-line arguments are
  74:      * found, suppresses the normal banner window.
  75:      */
  76:   public static void main(String[] args)
  77:     {   
  78:         Rpn the_calculator = new Rpn();
  79:         if( args.length <= 0 )
  80:             JOptionPane.showMessageDialog(null, about_message,
  81:                                     "About RPN Calculator",
  82:                                     JOptionPane.INFORMATION_MESSAGE );
  83:     }
  84: 
  85:     /************************************************************
  86:      * Calculator is a PAC "Control" class. The constructor
  87:      * first sets up its own visual environment
  88:      * by setting up the Menu_site and installing a few basic
  89:      * menu items (File:Exit and Help:About). (Other menu items
  90:      * will be added by the UI proxies as they are activated.)
  91:      *
  92:      * It then creates the two logical-model
  93:      * ("abstraction") entities (the Math_stack and Parser) that
  94:      * comprise the current application.
  95:      * 
  96:      * Finally, the control objects set's up the UI by
  97:      * requesting UI proxies ("presentations") from the two
  98:      * abstraction-level classes and installing them in the
  99:      * Calculator's frame and doing the necessary stuff to get
 100:      * the focus to the correct UI proxy.
 101:      */
 102:   public Rpn()
 103:     {
 104:         super       ("RPN Calculator"    );
 105:         setBounds   ( 200, 200, 200, 325 );
 106: 
 107:         // Set up the File and Help menus
 108: 
 109:         JMenu file = support.menu("File");
 110:         file.add( support.line_item
 111:                   ( "Exit",
 112:                     new ActionListener()
 113:                   {   public void actionPerformed(ActionEvent e)  
 114:                         {   System.exit(0);
 115:                         }
 116:                     }
 117:                   )
 118:                 );
 119: 
 120:         JMenu help = support.menu("Help" );
 121:         help.add( support.line_item
 122:                   ( "About RPN Calculator",
 123:                     new ActionListener()
 124:                   {   public void actionPerformed(ActionEvent e)  
 125:                         {   JOptionPane.showMessageDialog(
 126:                                     Rpn.this,
 127:                                     about_message,
 128:                                     "About RPN Calculator",
 129:                                     JOptionPane.INFORMATION_MESSAGE );
 130:                         }
 131:                     }
 132:                   )
 133:                 );
 134: 
 135:         add_menu( this, file );
 136:         add_menu( this, help );
 137: 
 138:         // Get the user interfaces provided by the parser and
 139:         // stack and arrange for the focus to stay in the Parser's
 140:         // UI at all times.
 141: 
 142:       JComponent stack_viewer = math_engine.visual_proxy("", false);
 143:         parser_viewer           = parser.     visual_proxy("", false);
 144:         
 145:         stack_viewer.addFocusListener
 146:         (   new FocusAdapter()
 147:           {   public void focusGained(FocusEvent e)
 148:                 {   parser_viewer.requestFocus();
 149:                 }
 150:             }
 151:         );
 152:         stack_viewer.addComponentListener
 153:         (   new ComponentAdapter()
 154:           {   public void componentResized(ComponentEvent e)
 155:                 {   parser_viewer.requestFocus();
 156:                 }
 157:             }
 158:         );
 159: 
 160:         // Display the parser and stack user interfaces in the
 161:         // calculator's frame.
 162: 
 163:         JSplitPane splitter = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
 164:         splitter.setTopComponent( stack_viewer );
 165:         splitter.setBottomComponent( parser_viewer );
 166:         getContentPane().add( splitter );
 167: 
 168:         // Arrange for the application to shut down when the main
 169:         // frame is closed. Also arrange for focus to be transferred
 170:         // to the Parser's UI when the main frame is activated.
 171: 
 172:         this.addWindowListener
 173:         (   new WindowAdapter()
 174:           {   public void windowClosing(WindowEvent e)
 175:                 {   System.exit(0);
 176:                 }
 177:               public void windowActivated(WindowEvent e)
 178:                 {   parser_viewer.requestFocus();
 179:                 }
 180:             }
 181:         );
 182: 
 183:         show();
 184:         parser_viewer.requestFocus();
 185:     }
 186:     /************************************************************
 187:      * Delegates menu-site operation to the similarly named method
 188:      * in the Menu_site.Support class.
 189:      * @see Menu_site
 190:      */
 191:   public void add_menu(Object requester, JMenu item)
 192:     {   support.add_menu(requester, item);
 193:     }
 194:     /************************************************************
 195:      * Delegates menu-site operation to the similarly named method
 196:      * in the Menu_site.Support class.
 197:      * @see Menu_site
 198:      */
 199:   public void 
 200:     add_line_item(Object requester, JMenuItem item, String to_this_menu)
 201:     {   support.add_line_item(requester, item, to_this_menu );
 202:     }
 203:     /************************************************************
 204:      * Delegates menu-site operation to the similarly named method
 205:      * in the Menu_site.Support class.
 206:      * @see Menu_site
 207:      */
 208:   public void remove_my_menus(Object requester)
 209:     {   support.remove_my_menus(requester);
 210:     }
 211: }
         

The Math stack

Now let's look at the classes that make up the abstraction-layer "model," starting with the Math_stack (Listing 2). The UML version of the static model is in Figure 7. The stack is made up of an array of doubles (stack: see Listing 2, line 51) and the stack pointer (sp: see Listing 2, line 53).

The constructor controls the size of the array. The math engine also supports 10 registers (accessed by number), implemented by an array of doubles (Math_stack register: see Listing 2, line 48).

Figure 7. The Math_stack arithmetic engine

The vast majority of Math_stack's methods just implement stack-manipulation requests (push, pop, add, and so on), so they don't need further comment. The proxy maintenance is worth looking at, however. The stack must notify the proxies whenever it changes state so that they can redraw themselves. It does this by means of Swing's ActionListener interface. The Stack_viewer proxies (Listing 2, line 86) implement ActionListener, and the Math_stack object keeps a list of the proxies (the Stack_viewers) in an AWTEventMulticaster called stack_proxies (Listing 2, line 58).

All of the stack-manipulation methods (such as push) finish up with a call to update() (Listing 2, line 178), which passes an ActionPerformed message to the multicaster, which in turn relays the message proxies. The visual_proxy(...) method (Listing 2, line 148), when it creates the proxies, both manufactures the proxy object and adds it to the multicaster.

The Stack_viewer inner class (Listing 2, line 86) is a Scrollable_JTextArea (presented in January's Java Toolbox). In Stack_viewer, the actionPerformed(...) method (Listing 2, line 112) performs the only real work by redrawing and nicely formatting the text area. (The Align class, used to align the numbers on the decimal point, was also presented in January's Java Toolbox.)

Note that the Stack_viewer is an inner-class object, so it has direct access to the actual stack and sp fields in the Math_stack outer class, and it indeed accesses these fields directly. Though such a tight coupling does violate the integrity of the outer-class object, which makes it suspect from an object-oriented perspective, all proxies are inherently tightly coupled to the objects they represent.

If you change the object, you'll have to change the proxies too. Since the coupling is inevitable, there's no point in adding complexity (additional methods) to present the illusion of decoupling. In any event, nonstatic inner classes are members of the outer class in a very real sense, so it is reasonable for them to use the access privilege available to all members. The maintenance problem inherent in this tight coupling is obviated to some extent by the fact that the two classes being declared in the same place. Consequently, when you make a change to the outer class, it's easy to find all the other affected classes -- they're all inner classes of the outer class.

Listing 2: /src/rpn/Math_stack.java

1: package rpn; 2: 3: import java.lang.*; 4: import java.util.*; 5: import java.text.*; 6: import java.awt.*; 7: import java.awt.event.*; 8: import javax.swing.*; 9: 10: import com.holub.ui.User_interface; 11: import com.holub.ui.Menu_site; 12: import com.holub.ui.Scrollable_JTextArea; 13: import com.holub.string.Align; 14: 15: import com.holub.tools.debug.Assert; 16: 17: /** © 1998, Allen I. Holub. All rights reserved. 18: *

19: * The Math_stack implements a fixed-size stack of doubles. It supports 20: * all operations supported by java.util.Stack() (empty, peek, pop, 21: * push, and search), though the arguments and return values are all 22: * double, and DOUBLE.NaN is returned in places where the standard 23: * stack would return null. As with the standard class, 24: * EmptyStackException is thrown if you try to pop from an empty stack. 25: * Math_stack.FullStackException is thrown if you try to push on a full 26: * stack (not needed in the standard implementation, which is based on 27: * a vector). Like the standard EmptyStackException, you should not 28: * usually catch the exception or mention it in a throws statement. 29: * (It's a RuntimeException.) Various mathematics functions are 30: * supported as well (documented below). 31: * 32: *

Revisions: 33: * 1/7/99 34: * Added the notions of registers, each of which 35: * can provide a visual proxy. There are ten 36: * registers named 0, 1, 2, ... 9. Store the TOS 37: * item in a register by pushing the register 38: * number and issuing a store() request. Recall 39: * the previous value by pushing the register number 40: * and issuing a recall() request. 41: * 42: * 43: * @author Allen Holub 44: * @version 1.1 45: **/ 46: 47: 48: public final class Math_stack implements User_interface 49: { 50: private int max_size; 51: private double[] stack; 52: private double[] register = new double[10]; // 10 registers 53: private int sp; 54: 55: // These will be AWTEvent Multicasters that keep track of the 56: // various proxies. 57: 58: private ActionListener stack_proxies = null; 59: 60: //--------------------------------------------------------------- 61: public static class FullStackException extends RuntimeException 62: { 63: public 64: String toString(){return "Push onto full Math_stack";} 65: } 66: 67: /******************************************************************* 68: * The Stack_viewer class is a small frame that shows the top few stack 69: * items. (You can resize it or scroll it to see the entire stack 70: * if you wish.) Modifications to the associated Math_stack are 71: * automatically displayed in the viewer with no action required 72: * on your part. You cannot declare a viewer. (Its constructor is 73: * private.) Get one from Math_stack.visual_proxy(). Unfortunately, 74: * even though the JTextArea is not editable, it will still 75: * capture the cursor if a user clicks in it. This problem can 76: * be (and is) addressed in the object that gets the viewer. 77: * 78: * A Stack_proxy is always treated as simple Components (i.e., they 79: * are accessed through a public interface), so the 80: * class itself can be made private. 81: * 82: * @see User_interface 83: * @see Math_stack#user_interface 84: **/ 85: 86: private class Stack_viewer extends Scrollable_JTextArea implements ActionListener 87: { 88: public Stack_viewer() 89: { 90: super(true); 91: 92: setVerticalScrollBarPolicy ( JScrollPane.VERTICAL_SCROLLBAR_ALWAYS ); 93: setHorizontalScrollBarPolicy( JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED ); 94: 95: setBackground( Color.lightGray ); 96: setForeground( Color.black ); 97: setFont ( new Font("Monospaced",Font.BOLD,11) ); 98: setLineWrap ( false ); 99: 100: setMinimumSize( new Dimension(200,100) ); 101: } 102: 103: /*************************************************************** 104: * Called when the stack being viewed has been modified, clears 105: * the entire text box, then draws the stack (with the top of 106: * stack item at the top of the window. Finally, it 107: * set the cursor ("Caret Position") to the top line, 108: * effectively scrolling the top-of-stack item back into 109: * view if the stack turns out to be larger than the window. 110: **/ 111: 112: public void actionPerformed( ActionEvent ignored ) 113: { 114: DecimalFormat compositor = new DecimalFormat("#,##0.00##"); 115: 116: JTextArea text_area = getTextArea(); 117: text_area.setText(""); 118: 119: for( int i = sp ; i < max_size; ++i ) 120: { 121: append( Align.align( compositor.format(stack[i]), 122: 20, // column width 123: 15, // alignment column 124: '.', // align on this character 125: ' ' // pad with spaces 126: )); 127: append( "\n" ); 128: } 129: text_area.setCaretPosition(0); 130: } 131: } 132: 133: /******************************************************************* 134: * Get a viewer proxy for the current stack. Delete it when you're 135: * done with it (or let the user get rid of it by clicking on the 136: * "close" box). 137: * 138: *

BUG

: 139: * The returned proxy will not be garbage collected until the 140: * associated Math_stack is destroyed unless you call {@link release_proxies}. 141: * 142: * @param type ignored 143: * @param parent the containing window 144: * @returns The user interface. 145: * @see Math_stack.Stack_viewer 146: **/ 147: 148:

public JComponent visual_proxy(String type, boolean is_read_only )

149: { 150: Stack_viewer viewer = new Stack_viewer(); 151: stack_proxies = AWTEventMulticaster.add(stack_proxies, viewer); 152: return viewer; 153: } 154: /******************************************************************* 155: * Release all proxies back to the Math_stack. The proxy becomes 156: * unusable after being released, so it's reasonable to set 157: * all references to it to null. 158: * 159: * @param proxy The proxy to release. The referenced proxy must 160: * have been returned from a previous call to 161: *

proxy()

. 162: */ 163: 164:

public final void release_proxies( Component proxy )

165: { stack_proxies = null; 166: } 167: 168: //--------------------------------------------------------------- 169:

private void needs( int elements_needed )

170: { 171: // Throws an EmptyStackException of there aren't 172: // elements_needed slots available on the stack. 173: 174: if( (sp + elements_needed) > max_size ) 175: throw new EmptyStackException(); 176: } 177: 178:

private void update( )

179: { 180: // Called by all methods that modify the stack just before 181: // they return. Notifies all view proxies (the observers) 182: // that the stack has changed so that they can redraw 183: // themselves. 184: 185: stack_proxies.actionPerformed( null ); 186: } 187: /******************************************************************* 188: * Creates a math stack (of doubles) of the given size. 189: */ 190:

public Math_stack( int max_size )

191: { this.max_size = max_size; 192: stack = new double[max_size]; 193: sp = max_size; 194: } 195: 196: /******************************************************************* 197: * Push an item on the stack 198: * @throws FullStackException 199: **/ 200:

public void push( double d )

201: { if( sp <= 0 ) 202: throw new FullStackException(); 203: 204: stack[ --sp ] = d; 205: update(); 206: } 207: 208: /******************************************************************* 209: * Pop an item from the stack and return it. 210: * @throws Math_stack.EmptyStackException 211: * @returns the popped item. 212: **/ 213:

public double pop()

214: { needs( 1 ); 215: double return_value = stack[ sp++ ]; 216: update(); 217: return return_value; 218: } 219: 220: /******************************************************************* 221: * @returns true if the stack is empty 222: */ 223:

public boolean empty() { return sp >= max_size; }

224: 225: /******************************************************************* 226: * @returns true if the stack is full 227: */ 228:

public boolean full() { return sp <= 0; }

229: 230: 231: /******************************************************************* 232: * @returns number of elements on the stack 233: */ 234:

public int has(){ return max_size - sp; }

235: 236: /******************************************************************* 237: * @returns the item at top of stack 238: */ 239:

public double peek() // throws EmptyStackException

240: { needs(1); 241: return stack[sp]; 242: } 243: 244: /******************************************************************* 245: * Search the stack from the top down looking for a match of 246: * look_for. 247: * 248: * @returns the distance from top of stack to the item or 249: * -1 if the item is not found. 250: **/ 251:

public int search( double look_for )

252: { for( int i = max_size; --i >= sp ; ) 253: if( stack[i] == look_for ) 254: return sp - i; 255: return -1; 256: } 257: 258: /******************************************************************* 259: * Replace the two items at top of stack with their sum. 260: * 261: * @throws EmptyStackException if there is one or fewer items on 262: * the stack. 263: **/ 264: 265:

public void add() // throws EmptyStackException

266: { needs(2); 267: stack[sp+1] += stack[sp]; 268: ++sp; 269: update(); 270: } 271: 272: /******************************************************************* 273: * Replace the two items at top of stack with their difference. 274: * (The item at top of stack is subtracted from the item just 275: * under it). 276: * 277: * @throws EmptyStackException if there is one or fewer items on 278: * the stack. 279: **/ 280: 281:

public void subtract() // throws EmptyStackException

282: { needs(2); 283: stack[sp+1] -= stack[sp]; 284: ++sp; 285: update(); 286: } 287: 288: /******************************************************************* 289: * Replace the two items at top of stack with their product. 290: * 291: * @throws EmptyStackException if there is one or fewer items on 292: * the stack. 293: **/ 294: 295:

public void multiply() // throws EmptyStackException

296: { needs(2); 297: stack[sp+1] *= stack[sp]; 298: ++sp; 299: update(); 300: } 301: 302: /******************************************************************* 303: * Replace the two items at top of stack with their quotient. 304: * (The item one cell down from top of stack is divided by the 305: * the item at top of stack). 306: * 307: * @throws EmptyStackException if there is one or fewer items on 308: * the stack. 309: **/ 310: 311:

public void divide() // throws EmptyStackException

312: { needs(2); 313: stack[sp+1] /= stack[sp]; 314: ++sp; 315: update(); 316: } 317: 318: /******************************************************************* 319: * TOS = -TOS 320: * @throws EmptyStackException 321: **/ 322:

public void invert()

323: { needs(1); 324: stack[sp] = -stack[sp]; 325: update(); 326: } 327: 328: /******************************************************************* 329: * TOS starts out with a time represented as hh.mmss. The TOS 330: * item is replaced with the same time represented in decimal. 331: * For example, the time of 6:30 is input as 6.30, executing 332: * convert_hms_to_decimal results in a the 6.30 being replaced 333: * by 6.50 (6 and 1/2 hours). 334: */ 335:

public void covert_hms_to_decimal()

336: { needs( 1 ); 337: 338: long time = (long)( Math.floor(stack[sp] * 10000.0) ); 339: 340: double seconds = time % 100; time /= 100; 341: double minutes = time % 100; time /= 100; 342: double hours = time; 343: 344: stack[sp] = hours + (minutes/60.0) + (seconds/(60.0 * 60.0)); 345: update(); 346: } 347: 348: /******************************************************************* 349: * TOS = square root of item at top of stack 350: * @throws EmptyStackException 351: **/ 352:

public void sqrt()

353: { needs(1); 354: stack[sp] = Math.sqrt(stack[sp]); 355: update(); 356: } 357: 358: /******************************************************************* 359: * Replaces top two stack items with TOS-1, raised to the power 360: * of TOS. 361: * @throws EmptyStackException 362: **/ 363:

public void pow()

364: { needs(2); 365: stack[sp+1] = Math.pow( stack[sp+1], stack[sp] ); 366: ++sp; 367: update(); 368: } 369: 370: /******************************************************************* 371: * Swap the two items at top of stack. 372: * 373: * @throws EmptyStackException if there is one or fewer items on 374: * the stack. 375: **/ 376: 377:

public void swap() // throws EmptyStackException

378: { needs(2); 379: 380: double tmp = stack[ sp ]; 381: stack[sp ] = stack[ sp+1 ]; 382: stack[sp+1] = tmp; 383: update(); 384: } 385: 386: /******************************************************************* 387: * Push a duplicate of the current top-of-stack item. 388: * 389: * @throws EmptyStackException if there is one or fewer items on 390: * the stack. 391: */ 392: 393:

public void duplicate() //throws EmptyStackException, FullStackException

394: { 395: push( peek() ); // Observers are notified by push 396: } 397: 398: /******************************************************************* 399: * Deletes all stack items. 400: */ 401: 402:

public void clear()

403: { 404: sp = max_size; 405: update(); 406: } 407: 408: /******************************************************************* 409: * Set the named register to a given value 410: */ 411:

public void register_set( int name, double value )

412: { register[name]=value; 413: } 414: 415: /******************************************************************* 416: * Return the value of the named register. 417: */ 418:

public double register_get( int name )

419: { return register[name]; 420: } 421: 422: /******************************************************************* 423: * Push the value of the named register onto the operand stack. 424: */ 425:

public void register_push( int name )

426: { push( register[name] ); 427: } 428: 429: /******************************************************************* 430: * Pop the top of stack item into the named register 431: */ 432:

public void register_pop( int name )

433: { register[name] = pop(); 434: } 435: 436: /******************************************************************* 437: * Save the top of stack item into the named register without popping. 438: */ 439:

public void register_save( int name )

440: { register[name] = peek(); 441: } 442: 443: /******************************************************************* 444: * A unit-test class. Main prints nothing if the stack is okay. 445: */ 446:

public static class Test

447: { 448:

public static void main( String[] args )

449: { 450: final Math_stack s = new Math_stack( 4 ); 451: 452: s.push( 1.0 ); 453: s.push( 2.0 ); 454: s.add(); 455: if( s.peek() != 3.0 ) 456: System.out.println("Math_stack: add" ); 457: 458: s.push( 2.0 ); 459: s.subtract(); 460: if( s.peek() != 1.0 ) 461: System.out.println("Math_stack: subtract" ); 462: 463: s.pop(); 464: if( !s.empty() ) 465: System.out.println("Math_stack: empty" ); 466: 467: s.push( 5 ); 468: s.duplicate (); 469: s.multiply(); 470: if( s.peek() != 25.0 ) 471: System.out.println("Math_stack: dup or multiply" ); 472: 473: s.push( 100.0 ); 474: s.swap(); 475: s.divide(); 476: 477: if( s.peek() != 4.0 ) 478: System.out.println("Math_stack: swap or divide" ); 479: 480: if( s.search(7) != -1 ) 481: System.out.println("Math_stack: search found nonexistent"); 482: 483: if( s.search(4) != 0 ) 484: System.out.println("Math_stack: search missed existent"); 485: 486: try 487: { 488: s.push(3.0); // two things 489: s.push(2.0); // two things 490: s.push(1.0); // two things 491: } 492: catch ( Exception e ) 493: { 494: System.out.println("Math_stack: Unexpected exception 1:"+e); 495: } 496: 497: try 498: { 499: s.push(0.0); // should cause overflow 500: System.out.println( "Math_stack: Overflow not caught\n"); 501: } 502: catch ( Math_stack.FullStackException e ){} 503: catch ( Exception e ) 504: { 505: System.out.println("Math_stack: Unexpected exception 2:"+e); 506: } 507: 508: try 509: { 510: if(s.pop() != 1.0)System.out.println("Math_stack: pop 1"); 511: if(s.pop() != 2.0)System.out.println("Math_stack: pop 2"); 512: if(s.pop() != 3.0)System.out.println("Math_stack: pop 3"); 513: if(s.pop() != 4.0)System.out.println("Math_stack: pop 4"); 514: if(!s.empty() )System.out.println("Math_stack: pop 5"); 515: } 516: catch ( Exception e ) 517: { 518: System.out.println("Math_stack: Unexpected exception 3:"+e); 519: } 520: 521: try 522: { 523: s.pop(); // should cause underflow 524: System.out.println( "Math_stack: Underflow not caught\n" ); 525: } 526: catch ( EmptyStackException e ){} 527: catch ( Exception e ) 528: { 529: System.out.println("Math_stack: Unexpected exception 4:"+e); 530: } 531: 532: s.push(9.0); 533: s.push(2.0); 534: s.pow(); 535: if( s.peek() != 81.0 ) 536: System.out.println("Math_stack: pow()"); 537: s.sqrt(); 538: if( s.peek() != 9.0 ) 539: System.out.println("Math_stack: sqrt()"); 540: 541: 542: System.out.println( "Testing visual components. Type x<enter>" 543: +" eight times.\n"); 544: 545: Frame f = new Frame(); 546: 547: f.setBounds ( 0, 0, 300, 300 ); 548: f.add ( "Center", s.visual_proxy(null,false) ); 549: f.show ( ); 550: 551: s.push( 1.0 ); 552: try{while(System.in.read() != 'x');}catch(Exception e){} 553: s.push( 2.0 ); 554: try{while(System.in.read() != 'x');}catch(Exception e){} 555: s.push( 3.0 ); 556: try{while(System.in.read() != 'x');}catch(Exception e){} 557: s.push( 4.0 ); 558: try{while(System.in.read() != 'x');}catch(Exception e){} 559: s.add (); 560: try{while(System.in.read() != 'x');}catch(Exception e){} 561: s.swap (); 562: try{while(System.in.read() != 'x');}catch(Exception e){} 563: s.subtract (); 564: try{while(System.in.read() != 'x');}catch(Exception e){} 565: s.multiply (); 566: try{while(System.in.read() != 'x');}catch(Exception e){} 567: 568: f.dispose(); 569: System.exit(0); 570: } 571: } 572: }

The Parser

Unlike the Math_stack, the Parser is fairly complicated. Let's start by looking at the pieces; then we'll put the pieces together.

The Keypad view

The first piece is the Calculator_keypad class (the UML is in Figure 8).

Figure 8. The keypad-style calculator UI

To refresh your memory, the keypad is shown in Figure 9.

Figure 9. Keypad screenshot

The keypad -- a JPanel -- uses a GridBag to lay itself out into two columns and five rows as seen in Figure 10 below.

Figure 10. The outermost panel

In Figure 10, the top row holds a JLabel accumulator, and the right columns of the next three rows hold the add, subtract, and multiply keys. The bottom row holds the "Enter" and "divide" keys.

The left column of the second, third, and fourth rows hold a sub-JPanel, which uses a second GridBag to lay itself out as a numeric keypad, as seen in Figure 11 below.

Figure 11. The calculator-keypad view's nested subpanel (numeric keys)

This organization -- a class both derives from and contains instances of another class, and objects of that class are organized in a containment relationship -- is an example of the Gang of Four Composite design pattern (for more on the Gang of Four, see Resources).

Other design patterns are represented here as well. For example, the Calculator_keypad and Tape widgets use the Gang of Four Observer pattern to communicate with the outside world. When an object wants to find out when a user enters text into either widget, the object expresses its interest by telling the widget to send a notification to the object. (It does so by calling addActionListener(...). See (Listing 3, line 142.) The widget notifies the listeners when text is available by sending them actionPerformed(...) messages.

The JButton objects, when pressed, also notify their listeners (the Observer pattern again), but the current implementation uses a single Listener object to mediate between the whole set of buttons and the Calculator_keypad object itself (Mediator). That is, all the buttons talk to a single Mediator object, whose job is to update the keypad as necessary. The Controller object's actionPerformed(...) method (Listing 3, line 171) does the work, intercepting button-press notifications and figuring out what to do with them. Numbers accumulate (and are echoed to the accumulator label) until a non-number is encountered, in which case an actionPerformed() message is sent to the keypad's observers. Non-numeric key presses are dispatched immediately (and the accumulator window is cleared).

Listing 3: /src/rpn/Calculator_keypad.java

1: package rpn; 2: 3: import java.awt.*; 4: import java.awt.event.*; 5: import javax.swing.*; 6: 7: /** A "lightweight" component that implements a simple calculator-style 8: * keypad. 9: * 10: * This class is an example of what JFC calls a "view-controller." 11: * It encapsulates both view (drawing) and controller (event-receiver) 12: * behavior in a single class. That is, it both causes several 13: * buttons to appear on the screen (the view) and it also catches 14: * the action events sent by the buttons (the controller). 15: *

16: * The generated keypad looks like this: 17: *

18:  *
19:  *
  20:  **/
  21: 
  22: public class Calculator_keypad extends JPanel
  23: {
  24:     JLabel     accumulator = new JLabel("")
  25:                              {
  26:                               public void paint(Graphics g)
  27:                                 {
  28:                                     g.setColor( Color.black );
  29:                                     Dimension size = getSize(); 
  30:                                     g.fillRect( 0, 0, size.width, size.height );
  31:                                     super.paint( g );
  32:                                 }
  33:                              };
  34: 
  35:     Controller controller  = new Controller();  // an inner class,
  36:                                                 // defined below.
  37:     //----------------------------------------------------------------
  38:     // Create the keypad. To simplify the layout, we create an inner
  39:     // container that holds the numeric buttons, and an outer
  40:     // container that holds the inner one, along with a few other
  41:     // buttons for the + - * / and "Enter" key, and also a "label"
  42:     // that shows the partially-accumulated input string.
  43: 
  44:     Calculator_keypad()
  45:     {
  46:         accumulator.setForeground   (Color.green);
  47:         accumulator.setFont         (new Font("Monospaced",Font.BOLD, 14));
  48:         accumulator.setPreferredSize(new Dimension(100, 20) );
  49: 
  50:         JPanel keypad = new JPanel();
  51:         keypad.setLayout( new GridBagLayout() );
  52:         //        cont.   r  c  w  h  x  y  p  label/obj, action
  53:         add_cell( keypad, 2, 0, 1, 1, 1, 1, 0, "1"       , "1" );
  54:         add_cell( keypad, 2, 1, 1, 1, 1, 1, 0, "2"       , "2" );
  55:         add_cell( keypad, 2, 2, 1, 1, 1, 1, 0, "3"       , "3" );
  56:         add_cell( keypad, 1, 0, 1, 1, 1, 1, 0, "4"       , "4" );
  57:         add_cell( keypad, 1, 1, 1, 1, 1, 1, 0, "5"       , "5" );
  58:         add_cell( keypad, 1, 2, 1, 1, 1, 1, 0, "6"       , "6" );
  59:         add_cell( keypad, 0, 0, 1, 1, 1, 1, 0, "7"       , "7" );
  60:         add_cell( keypad, 0, 1, 1, 1, 1, 1, 0, "8"       , "8" );
  61:         add_cell( keypad, 0, 2, 1, 1, 1, 1, 0, "9"       , "9" );
  62:         add_cell( keypad, 3, 0, 2, 1, 2, 1, 0, "0"       , "0" );
  63:         add_cell( keypad, 3, 2, 1, 1, 1, 1, 0, "."       , "." );
  64: 
  65:         this.setLayout( new GridBagLayout() );
  66: 
  67:         add_cell( this,   0, 0, 2, 1, 4, 1, 3, accumulator, null );
  68:         add_cell( this,   1, 1, 1, 1, 1, 1, 3, "+"        , "+"  );
  69:         add_cell( this,   2, 1, 1, 1, 1, 1, 3, "-"        , "-"  );
  70:         add_cell( this,   3, 1, 1, 1, 1, 1, 3, "X"        , "*"  );
  71:         add_cell( this,   4, 1, 1, 1, 1, 1, 3, "/"        , "/"  );
  72:         add_cell( this,   4, 0, 1, 1, 4, 1, 3, "Enter"    , "\n" );
  73:         add_cell( this,   1, 0, 1, 3, 3, 4, 3, keypad     , null );
  74: 
  75:         setMinimumSize( getPreferredSize() );
  76:         setMaximumSize( getPreferredSize() );
  77:     }
  78:     //----------------------------------------------------------------
  79:   private void add_cell(  Container container,
  80:                             int row,    int col, 
  81:                             int width,  int height,
  82:                             int weightx,int weighty,
  83:                             int pad,
  84:                             Object item,
  85:                             String action_command )
  86:     {
  87:         GridBagConstraints constraints = new GridBagConstraints();
  88: 
  89:         constraints.fill        = GridBagConstraints.BOTH;
  90:         constraints.gridy       = row;
  91:         constraints.gridx       = col;
  92:         constraints.gridwidth   = width;
  93:         constraints.gridheight  = height;
  94:         constraints.weightx     = weightx;
  95:         constraints.weighty     = weighty;
  96:         constraints.insets      = new Insets(pad,pad,pad,pad);
  97: 
  98:         JComponent component;
  99:         if( item instanceof JComponent )
 100:             component = (JComponent)item;   
 101:         else                            // assume it's a string
 102:         {
 103:             // The "action command" holds the command sent to
 104:             // parse() to handle the button press. For the time
 105:             // being, that's the same as the button label, but it
 106:             // doesn't have to be. All buttons notify the same
 107:             // controller object.
 108: 
 109:             String label = (String) item;
 110:             JButton button = new JButton( label          );
 111:             button.addActionListener    ( controller     );
 112:             button.setActionCommand     ( action_command );
 113:             button.setFont
 114:             (   new Font("SansSerif",Font.BOLD,
 115:                 Character.isLetterOrDigit(label.charAt(0))? 12: 16 )
 116:             );
 117:             component = button;
 118:         }
 119: 
 120:         container.add( component );
 121:         component.setVisible( true );
 122: 
 123:         GridBagLayout layout = (GridBagLayout)container.getLayout();
 124:         layout.setConstraints( component, constraints );
 125:     }
 126:     //----------------------------------------------------------------
 127:   private ActionListener observers = null;
 128: 
 129:     /** The Calculator_keypad notifies action listeners when it
 130:      *  has accumulated a string for the listener to process. The
 131:      *  string itself is available as the "action command." Notifications
 132:      *  are sent when the user clicks on any of the operator keys
 133:      *  (+ - * /) or Enter. Numbers and decimal points are just
 134:      *  accumulated, and will be sent along with the arithmetic
 135:      *  character when the observers are notified. For example, if
 136:      *  you type a 1, 2, 3, *, the action event is sent when the * is
 137:      *  typed, and the action command is "123*". An "Enter" not
 138:      *  preceded with a number causes an empty-string action command
 139:      *  to be sent.
 140:      */
 141: 
 142:   public void addActionListener(ActionListener observer)
 143:     {   observers = AWTEventMulticaster.add(observers, observer);
 144:     }
 145: 
 146:   public void removeActionListener(ActionListener observer)
 147:     {   observers = AWTEventMulticaster.remove(observers, observer);
 148:     }
 149: 
 150:     //----------------------------------------------------------------
 151:     // The "controller" functionality. Receives notifications of button
 152:     // presses, accumulates them if necessary, and dispatches them off
 153:     // to the observers at appropriate times. Implementing the controller
 154:     // as a private inner class is better design than simply having
 155:     // Calculator_keypad implement Action Listener. The fact that
 156:     // a Calculator_keypad is an ActionListener is an implementation
 157:     // detail so it shouldn't be exposed publicly. The inner class also
 158:     // lets us effectively make the actionPerformed() method private.
 159:     // Otherwise actionPerformed() would have to be a public method
 160:     // of Calculator_keypad -- bad design because the outside
 161:     // world shouldn't know that the method exists, much less
 162:     // be able to call it. The "Controller" is not static since it
 163:     // accesses the "view" directly. This strong coupling is reasonable
 164:     // only because the Controller is effectively a member of
 165:     // Calculator_keypad, in the same way that a method is a member.
 166:      
 167:   private class Controller implements ActionListener
 168:     {
 169:       private final StringBuffer number_buffer = new StringBuffer();
 170: 
 171:       public void actionPerformed( ActionEvent e )
 172:         {   
 173:             if( observers != null )
 174:             {   String command = ((JButton)(e.getSource())).getActionCommand();
 175: 
 176:                 // Append text associated with the current button to
 177:                 // the number_buffer. (Append a space character for
 178:                 // the "Enter" key, but only if there's nothing in the
 179:                 // buffer.)
 180: 
 181:                 if( !command.equals("\n") )
 182:                     number_buffer.append( command );
 183: 
 184:                 else if( number_buffer.length() == 0 )
 185:                     number_buffer.append( " " );
 186: 
 187:                 // If the command is a dot or a base-10 digit, we're
 188:                 // accumulating a number -- display the current number_buffer
 189:                 // on the accumulator window. Otherwise, a non-number
 190:                 // was received: send the buffer off to the parser in
 191:                 // an ActionEvent, then clear the buffer.
 192: 
 193:                 if ( command.equals(".")
 194:                            || Character.digit(command.charAt(0),10)!=-1 )
 195:                 {
 196:                     accumulator.setText( " " + number_buffer.toString() );
 197:                 }
 198:                 else
 199:                 {   accumulator.setText( "" );
 200:                     observers.actionPerformed(
 201:                                 new ActionEvent(this, 0,
 202:                                                 number_buffer.toString())); 
 203:                     number_buffer.setLength(0);
 204:                 }
 205:             }
 206:         }
 207:     }
 208: 
 209:     /****************************************************************
 210:      *  The Test inner class encapsulates a main() that tests the
 211:      *  Calculator_keypad. I'm not putting main() into Calculator_keypad
 212:      *  itself because I don't want the test code in the final
 213:      *  application. A separate class creates a separate .class file
 214:      *  (called Calculator_keypad$Test.class) which does not have
 215:      *  to be included in the release version of the program.
 216:      **/
 217:   static public class Test
 218:     {
 219:       public static void main( String[] args )
 220:         {   JFrame frame = new JFrame();
 221:             Calculator_keypad keyboard = new Calculator_keypad();
 222: 
 223:             keyboard.addActionListener
 224:             (   new ActionListener()
 225:               {   public void actionPerformed( ActionEvent e )
 226:                     {   System.out.println( e.getActionCommand() );
 227:                     }
 228:                 }
 229:             );
 230: 
 231:             frame.addWindowListener
 232:             (   new WindowAdapter()
 233:               {   public void windowClosing( WindowEvent e )
 234:                     {   System.exit(0);
 235:                     }
 236:                 }
 237:             );
 238: 
 239:             frame.getContentPane().add( keyboard );
 240:             frame.pack();
 241:             frame.show();
 242:         }
 243:     }
 244: }   
         

The tape view

The tape-style view is also straightforward, though it uses a somewhat different internal architecture than does the keypad view (see Figure 12). As is the case with the keypad, the Tape is a Jpanel, but it contains only two fields: a JTextField (input: see Listing 4, line 30) into which you type your input and a Scrollable_JTextArea (output: see Listing 4, line 31), which stores a record of your data entry in a manner similar to the tape on a tape calculator. A log file (log: see Listing 4, line 33) -- implemented using the Log class discussed in January's Java Toolbox -- also stores the contents of this window. As is also the case with the keypad, any object interested in input from this view should call addActionListener(...) (Listing 4, line 191) to register as an ActionListener in the observers list.

Figure 12. The tape-style calculator UI

In contrast to the keypad view with its listener-based strategy, the tape view employs a Swing-model object to discover when the user enters characters. For reasons that are not clear to me, the JTextField doesn't support the AWT TextField's TextListener (which was notified when new text was entered into the control). Instead, JTextField supports an extremely complex set of listeners such as the CaretListener, which is notified when the cursor position changes, but none of these let you easily find out when the text changes.

The easiest way to trap character-by-character text entry is to provide an implementation of the PlainDocument -- the Swing model associated with the text objects -- and override its insertString() method, which is called when a character is inserted into the string. I've done that in insertString(...) (Listing 4, line 94), which works much like the keypad's Controller object, accumulating alphanumeric strings until it encounters a non-alphanumeric character, but processing non-alphanumerics immediately. The Model object calls process_newline() (Listing 4, line 123) to dispatch the string off to the listeners by sending an actionPerformed() message to the observers multicaster (Listing 4, line 190).

An instance of the model is installed into the JTextArea UI delegate when the object is created on line 30. (See Resources for more on the Swing architecture.)

Another main difference between the keypad and the tape view is that you can write to a tape view. (The messages are displayed on the tape.) There are two string overrides of write: They are write(String,Color) (Listing 4, line 142) and write(String) (Listing 4, line 145). There's another for printing numbers (write(double): see Listing 4, line 154). Strings are just appended to the current line buffer (buffer: see Listing 4, line 32) and flushed to the tape when a new line is encountered. Numbers are formatted consistently and then appended to the buffer.

Listing 4: /src/rpn/Tape.java
   1: package rpn;
   2: 
   3: import java.awt.*;
   4: import javax.swing.*;
   5: import javax.swing.text.*;
   6: import java.awt.event.*;
   7: import java.util.*;
   8: import java.io.*;
   9: import java.text.*;
  10: 
  11: import com.holub.tools.debug.Assert;
  12: import com.holub.ui.Scrollable_JTextArea;
  13: import com.holub.io.Log;
  14: 
  15: import com.holub.string.Align;
  16: 
  17: /** A Tape is a 2-D text control for use in a calculator-style
  18:  *  application. It does several things:
  19:  *  
  1. 20:  *
  2. Accumulates all input strings until an 21:  * Enter or arithmetic operator (* - / *) is entered. 22:  *
  3. Notifies any ActionListeners when an 23:  * Enter or arithmetic operator (* - / *) is entered. The 24:  * accumulated string is available in the action command. 25:  *
  26:  **/
  27: 
  28: public class Tape extends JPanel
  29: {
  30:   private final JTextField           input =new JTextField(new Model(),"",128);
  31:   private final Scrollable_JTextArea output=new Scrollable_JTextArea(true);
  32:   private final StringBuffer         buffer=new StringBuffer();
  33:   private static final Log           log   =new Log();
  34: 
  35:   public Tape()
  36:     {   
  37:         output.setVerticalScrollBarPolicy
  38:                                 (JScrollPane.VERTICAL_SCROLLBAR_ALWAYS );
  39:         output.setHorizontalScrollBarPolicy
  40:                             (JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
  41: 
  42:         output.setBackground( new Color(255, 255, 204) ); // light yellow
  43:         output.setForeground( Color.black );
  44:         output.setFont     ( new Font("Monospaced",Font.BOLD,11) );
  45:         output.setLineWrap  ( false );
  46: 
  47:         input.addActionListener
  48:         (   new ActionListener()
  49:           {   public void actionPerformed(ActionEvent e)
  50:                 {   process_newline();
  51:                 }
  52:             }
  53:         );
  54: 
  55:         addFocusListener
  56:         (   new FocusAdapter()
  57:           {   public void focusGained(FocusEvent e)
  58:                 {   input.requestFocus();
  59:                 }
  60:             }
  61:         );
  62: 
  63:         output.addFocusListener     // Not working for some reason
  64:         (   new FocusAdapter()
  65:           {   public void focusGained(FocusEvent e)
  66:                 {   input.requestFocus();
  67:                 }
  68:             }
  69:         );
  70: 
  71:         setMinimumSize  ( new Dimension(800,100) );
  72:         setPreferredSize( new Dimension(800,100) );
  73:         setSize         ( new Dimension(800,100) );
  74: 
  75:         setLayout( new BorderLayout() );
  76:         add( output, "Center" );
  77:         add( input,  "South"  );
  78:         setVisible( true );
  79:     }
  80: 
  81:     /*******************************************************************
  82:      **/
  83: 
  84:   private final class Model extends PlainDocument
  85:     {
  86:         /** Called when a character is inserted into the string, either
  87:          *  by typing or by a paste operation.
  88:          *
  89:          * @param   value   holds inserted text
  90:          * @param   offset  is the position of the first character of the
  91:          *                  insertion (first character in string is 0).
  92:          * @param   a       is not used.
  93:          */
  94:       public void insertString(int offs, String value, AttributeSet a)
  95:                                             throws BadLocationException
  96:         {
  97:             if (value == null || (value.length() <= 0) )
  98:                 return;
  99: 
 100:             // Must chain to base-class version to get the characters
 101:             // to actually appear in the control.
 102: 
 103:             super.insertString(offs, value, a);
 104: 
 105:             // Alphanumerics are accumulated until a non-alpha is
 106:             // entered. Then the listeners are signaled.
 107: 
 108:             char last = value.charAt( value.length() -1 );
 109:             if ( !is_number(last) )
 110:                 process_newline();
 111: 
 112:         }
 113: 
 114:         /***************************************************************
 115:          * Returns true if the character is legitimate in a number
 116:          * (a digit or decimal point).
 117:          */
 118:       private final boolean is_number( char c )
 119:         {   return c=='.' || Character.isDigit©;
 120:         }
 121:     }
 122: 
 123:   private void process_newline()
 124:     {   String line = input.getText();
 125:         input.setText("");
 126:         observers.actionPerformed( new ActionEvent( Tape.this,0, line));
 127:     }
 128: 
 129:     //------------------------------------------------------------
 130:   public void requestFocus()
 131:     {   input.requestFocus(); // Transfer focus to the input window
 132:     }
 133: 
 134:     /****************************************************************
 135:      * Write a string on the tape. Text is not
 136:      * written to the tape until a "\n" is appended. Appended
 137:      * strings are treated just as if a user had typed them.
 138:      * @param text The string to append
 139:      * @param color The color to print the string (currently not
 140:      *              supported).
 141:      */
 142:   public void write( String text, Color color )
 143:     {   do_write(text, color);
 144:     }
 145:   public void write( String text )
 146:     {   do_write(text, Color.black );
 147:     }
 148: 
 149:     /****************************************************************
 150:      * A variant on write(String,Color) that appends a double.
 151:      * The color is red if the number is negative, black if it's
 152:      * positive.
 153:      */
 154:   public void write( double value )
 155:     {
 156:         DecimalFormat compositor = new DecimalFormat("#,##0.00##");
 157:         String formatted = Align.align( compositor.format( value ),
 158:                                            16,  // output column width
 159:                                            11,  // alignment column
 160:                                            '.', // align on this character
 161:                                            ' '  // pad with spaces
 162:                                            );
 163: 
 164:         do_write( formatted, value < 0 ? Color.red : Color.black );
 165:     }
 166: 
 167:     //------------------------------------------------------------
 168:     // Write the screen into the output window (the "tape")
 169: 
 170:   private void do_write( String text, Color color/*ignored*/ )
 171:     {   
 172:         buffer.append(text);
 173:         if( text.endsWith( "\n" ) )
 174:         {   String padded = Align.right( buffer.toString(), 22 );
 175:             output.append( padded );
 176:             log.write    ( padded );
 177: 
 178:             buffer.setLength(0);
 179:         }
 180:     }
 181:     //------------------------------------------------------------
 182:   public Dimension getPreferredSize()
 183:     {   return new Dimension( /*width*/ 100, /*height*/ 50 );
 184:     }
 185:     //------------------------------------------------------------
 186:   public Dimension getMinimumSize()
 187:     { return getPreferredSize();
 188:     }
 189:     //----------------------------------------------------------------
 190:   private ActionListener observers = null;
 191:   public void addActionListener(ActionListener observer)
 192:     {   observers = AWTEventMulticaster.add(observers, observer);
 193:     }
 194:   public void removeActionListener(ActionListener observer)
 195:     {   observers = AWTEventMulticaster.remove(observers, observer);
 196:     }
 197: }
         

The Parser itself

Now we can look at the Parser class itself, in Listing 5.

You'll find the UML in Figure 13. Starting at the UML's bottom, the Parser needs to talk to both the Keypad_viewer and the Tape_viewer in a consistent way. Though these classes are similar, the Tape supports write() methods and the Calculator_keypad doesn't. I'm reluctant, in a situation such as this, to introduce bogus write() methods to a class like Calculator_keypad, which has no possible valid implementation of those methods. The only robust implementation is to throw an exception if the bogus method is called, but I don't like to move a compile-time error ("method not found") into a runtime error (the exception toss).

The problem can be solved with the Gang of Four Adapter pattern, in this case a Class Adapter. Viewer_ui (Listing 5, line 264) defines a uniform interface to the two view classes. I then implement the interface in two adapters: The Keypad_viewer (Listing 5, line 272) is a Calculator_keypad (it extends Calculator_keypad) that implements the Viewer_ui, and Tape_viewer (Listing 5, line 278) is a Tape that implements the Viewer_ui. The Keypad_viewer implements empty write methods, since in the current context it isn't an error to throw away the output. The Tape_viewer interestingly, has no methods at all: all the methods that the Viewer_ui interface requires are inherited from the Tape base class. Java, unlike C++, doesn't need us to supply any derived-class methods in this situation.

Figure 13. The Parser input processor

As we see at the center of Figure 13, the Parser's visual proxy is an instance of Viewer (Listing 5, line 288), which is a JPanel that contains either a Calculator_keypad or a Tape, depending on which UI the user requests. The Viewer's other roles include: installing the Interface menu we discussed earlier on the main-frame's menu bar and dynamically updating the menu bar with the menu items appropriate for the current view. Finally, the Parser can write messages on the Viewer, which relays the messages to the current contained view object.

The Viewer keeps a reference to the current view in view (Listing 5, line 291), which is a reference to a Viewer_ui -- the common interface supported by the two adapters. Consequently, it doesn't need to know which concrete class it's talking to. The Viewer's constructor (Listing 5, line 293) installs an AncestorListener that initializes Menu_site to point at the main frame when the proxy is installed into the frame. The constructor also installs an instance of the tape-style view by calling use_tape() (Listing 5, line 372). Additional construction happens in addNotify() (Listing 5, line 484), which is called when the Viewer's Panel component is realized on the screen. The make_menu() method (Listing 5, line 492) creates the "Interface" menu, and sets things up so that use_tape() is called when the user requests a tape view and use_keypad() (Listing 5, line 392) is called when the user wants a keypad view. The matching removeNotify() method (Listing 5, line 525), called when the JPanel is destroyed, removes the "Interface" menu.

The use_tape() method (Listing 5, line 372) first tries to install a tape-style view by calling replaced_view_with(...) (Listing 5, line 337). This method is called whenever the user requests (by selecting the appropriate menu item) that the view change. It's passed the Class object for the requested view, and if an object of that class isn't being used as the current view, an object is created and installed, replacing the earlier view. Any menus installed by the previous view are also destroyed at this time. The replaced_view_with() returns false if the view wasn't replaced, in which case use_tape() does nothing. Otherwise, it modifies the menu bar to hold items appropriate to the tape-style view. The use_keypad() (Listing 5, line 392) method does essentially the same thing, though the menus it creates are different from the ones created by use_tape().

Moving over to the left side of Figure 13, we see that the Tape and Calculator keypad both notify listeners when they have input by sending them an actionPerformed() message. The mediator object (Listing 5, line 319) (an ActionListener) is notified when text is available, and this mediator relays it to the Parser's parse(...) method (Listing 5, line 110), which does the actual work. The Viewer installs the mediator into a view (the Tape or Calculator_keypad object) every time a new view is installed (on line 352). So, when the current Viewer's view has text for the parser, it calls parse(...), which does a brute-force parse of the input string and makes appropriate calls to the Math_stack that was specified in the constructor.

Listing 5: /src/rpn/Parser.java

1: package rpn; 2: 3: import javax.swing.*; 4: import javax.swing.event.*; 5: import java.util.*; 6: import java.awt.*; 7: import java.awt.event.*; 8: import java.text.*; 9: 10: import com.holub.ui.Scrollable_JTextArea; 11: import com.holub.ui.User_interface; 12: import com.holub.ui.Menu_site; 13: import com.holub.ui.AncestorAdapter; 14: import com.holub.tools.debug.Assert; 15: 16: //---------------------------------------------------------------- 17: 18: class Parser implements User_interface 19: { 20: // The "Parser" object handles user input to the calculator. 21: // It is notified every time the user enters a line. The 22: // Parser parses the line and sends appropriate messages to 23: // the associated Math_stack. The Math_stack object 24: // automatically updates its viewer when the stack changes 25: // state. 26: // 27: // The Parser is an "ActionListener" so that the TextField 28: // created by proxy, below, can talk to it. The Parser is 29: // sent an actionPerformed message every time the user types 30: // <Enter> in the TextField 31: // 32: // The parser is coupled tightly to the outer class in that 33: // it accesses the Math_stack field of the Calculator 34: // directly. This coupling could be avoided by passing the 35: // stack to the parser in a constructor argument, but it 36: // seemed cleaner to allow direct access since this is, after 37: // all, an inner class. 38: 39: private Viewer my_ui = null; 40: private Math_stack stack = null; 41: 42: //---------------------------------------------------------------- 43: // These are the commands recognized by the parser 44: 45: private static final char ADD = '+'; 46: private static final char SUBTRACT = '-'; 47: private static final char MULTIPLY = '*'; 48: private static final char DIVIDE = '/'; 49: private static final char DUPLICATE = ' '; 50: private static final char POW = '^'; 51: private static final char INVERT = '~'; 52: private static final char SUBTOTAL = '='; 53: private static final char CLEAR = 'c'; 54: private static final char DROP = 'd'; 55: private static final char HMS2DEC = 'm'; 56: private static final char QUIT = 'q'; 57: private static final char SQRT = 's'; 58: private static final char TOTAL = 't'; 59: private static final char SWAP = 'w'; 60: private static final char HELP = '?'; 61: 62: //---------------------------------------------------------------- 63: public Parser( Math_stack stack ) 64: { this.stack = stack; 65: } 66: //---------------------------------------------------------------- 67: public JComponent visual_proxy(String ignored, boolean is_constant ) 68: { 69: // Overrides User_interface.user_interface() 70: 71: if( my_ui != null ) 72: return null; 73: 74: return (my_ui = new Viewer()); 75: } 76: /******************************************************************* 77: * Parse a line of input. This method takes care of the requirements 78: * of a tape calculator (of which the Math_stack, which is just 79: * a stack-based math engine, knows nothing). The main job is to 80: * break up input lines into individual tokens. 81: * A blank line is treated as a "Total" request. This 82: * is awkward for HP-calculator users who expect Enter 83: * to do a "duplicate" operation, but it's essential for 84: * tape-calculator users, who need a "total" key right 85: * on the numeric keypad. The space bar does a "duplicate." 86: *

87: * The parser is pretty unforgiving about its input format. The 88: * "text" argument should be made up of a single, optional, number 89: * followed by an optional command. Multiple commands (and multiple 90: * numbers) on a single line are not supported. 91: * 92: * All communication with the math stack occurs here. The only 93: * weirdness is multiple request for arithmetic operations or 94: * totals, without intervening numbers: 95: * 96: * The most recently entered is the number most recently entered 97: * by the user. 98: * 99: * When a total is requested, the total replaces the most recently 100: * entered number. 101: * 102: * o If the stack is empty, zero is pushed before any operation 103: * is performed. 104: * 105: * o Then, if the stack contains only one item, and a binary operation 106: * is requested, the most recently entered number is pushed 107: * before the operation is performed. 108: */ 109: 110:

public void parse( String text )

111: { 112: text = text.toLowerCase(); 113: if( text.length() == 0 ) // Blank line was entered. 114: text = "t"; // Treat it as a request for a total. 115: 116: char command = text.charAt( text.length() -1 ); 117: boolean command_present = command != '.' && !Character.isDigit(command); 118: boolean is_binary_command = command==ADD || command==SUBTRACT || 119: command==MULTIPLY || command==DIVIDE || 120: command==POW || command==SWAP ; 121: 122: Number n = NumberFormat.getInstance() 123: .parse(text, new ParsePosition(0)); 124: 125: if( stack.empty() ) 126: stack.push(0.0); 127: 128: if( n != null ) // line starts with a number 129: { 130: stack.push( n.doubleValue() ); 131: my_ui.write( n.doubleValue() ); 132: 133: if( !command_present ) 134: my_ui.write( " push\n" ); 135: } 136: 137: if( command_present ) 138: { 139: if( is_binary_command && stack.has() < 2 ) 140: { stack.push ( most_recent ); 141: my_ui.write( most_recent ); 142: } 143: 144: switch( command ) 145: { 146: case ADD : stack.add(); my_ui.write(" + "); break; 147: case SUBTRACT: stack.subtract(); my_ui.write(" - "); break; 148: case MULTIPLY: stack.multiply(); my_ui.write(" * "); break; 149: case DIVIDE : stack.divide(); my_ui.write(" / "); break; 150: case DUPLICATE: stack.duplicate(); my_ui.write(" dup "); break; 151: case POW : stack.pow(); my_ui.write(" pow "); break; 152: case INVERT : stack.invert(); my_ui.write(" neg "); break; 153: case SUBTOTAL: my_ui.write( stack.peek() ); my_ui.write(" S "); break; 154: case CLEAR : stack.clear(); my_ui.write(" clr "); break; 155: case DROP : stack.pop(); my_ui.write(" drop"); break; 156: case HMS2DEC : stack.covert_hms_to_decimal(); my_ui.write(" hms "); break; 157: case QUIT : System.exit(0); my_ui.write(" exit"); break; 158: case SQRT : stack.sqrt(); my_ui.write(" sqrt"); break; 159: 160: case TOTAL : my_ui.write( stack.peek() ); my_ui.write(" = "); 161: most_recent = stack.peek(); 162: stack.clear(); 163: break; 164: 165: case SWAP : stack.swap(); my_ui.write(" swap"); break; 166: case HELP : popup_help(); break; 167: } 168: 169: my_ui.write("\n"); 170: } 171: 172: if( n != null ) // line starts with a number 173: most_recent = n.doubleValue(); 174: 175: if( stack.empty() ) 176: stack.push(0.0); 177: } 178: 179:

private double most_recent = 0.0 ; // most recently entered number

180:

private char last_command = '\0';

181: 182: /***************************************************************** 183: * Pop up a help window. This is just a normal (Modeless) frame with 184: * a scrollable TextArea in it. That way you can leave the help 185: * window up while you're working. I didn't want to use a 186: * JOptionPane because these are Modal. 187: */ 188:

public void popup_help()

189: { 190: JFrame help_window = new JFrame( "RPN Calculator Help" ); 191: Scrollable_JTextArea text = new Scrollable_JTextArea(usage_text, true); 192: 193: text.getTextArea().setEditable( false ); 194: help_window.getContentPane().add( text ); 195: 196: help_window.pack(); 197: help_window.show(); 198: } 199: 200:

private static final String[] usage_text =

201: { 202: "THIS WINDOW WILL REMAIN VISIBLE UNTIL YOU CLOSE IT. YOU MAY USE", 203: " THE CALCULATOR WHILE THE WINDOW IS DISPLAYED", 204: "", 205: "This application was downloaded from Allen Holub's web site:", 206: "", 207: " http://www.holub.com", 208: "", 209: "where you'll find information about Java-and-object-oriented training", 210: "and other java-related goodies. The .class files that", 211: "comprise this application may be distributed freely for", 212: "noncommercial purposes, provided that they are distributed", 213: "without modification.", 214: "", 215: "The \"good\" interface simulates a tape-style adding machine.", 216: "The tape appears on the screen, and can also be written to a file", 217: "if you specify -Dlog.file=name on the VM command line.", 218: "", 219: "Recognized commands are:", 220: " TOS is the item at top of stack", 221: " TOS-1 is the item below that.)", 222: "", 223: "<number> A line containing only a number causes that number", 224: " to be pushed.", 225: "<space> Push duplicate of the top-of-stack item", 226: "<blank> (blank line) same as " + TOTAL + ".", 227: ADD + " Add Replace TOS with TOS-1 + TOS", 228: SUBTRACT + " Subtract Replace TOS with TOS-1 - TOS", 229: MULTIPLY + " Multiply Replace TOS with TOS-1 * TOS", 230: DIVIDE + " Divide Replace TOS with TOS-1 / TOS", 231: POW + " Power Replace TOS with TOS-1 to the power of TOS", 232: INVERT + " Invert Change sign of TOS item", 233: SUBTOTAL + " Subtotal Print current TOS on tape", 234: CLEAR + " Clear clear stack and push 0.", 235: DROP + " Drop Delete TOS item", 236: HMS2DEC + " Time Replace time at TOS (HH.MMSS) with decimal (HH.xxx).", 237: QUIT + " Quit Terminate the program", 238: SQRT + " Square root Replace TOS with square root of TOS", 239: TOTAL + " Total Print total on tape, then clear stack", 240: SWAP + " Swap Swap top two stack items", 241: HELP + " Help Display this message", 242: "", 243: "In general, a tape-style calculator works like an HP-style RPN", 244: "calculator. However, certain behavior will be surprising to", 245: "users of HP RPN calculators. In particular, when", 246: "the stack doesn't contain sufficient operands for a given operation,", 247: "the most recently pushed number or the most-recent total is used,", 248: "for the second operand. If the stack is empty and a binary operation,", 249: "is requested 0 is used for the first operand and the most recently pushed", 250: "number or most recent total is used for the second operand.)", 251: "For example, in an HP RPN calculator, you could add a number", 252: "to itself three times with 123<Enter><Enter><Enter>+++. This will work,", 253: "however a tape-calculator-style operation (123+++) will do the same thing.", 254: "Hitting the total key (Enter) twice sets the value of the", 255: "\"most recently used number\" to zero." 256: }; 257: 258: //================================================================ 259: // Use the Actual_viewer interface to allow a Calculator_keypad 260: // and a Tape_viewer to be treated identically elsewhere 261: // in the code. 262: 263:

private static

264:

interface Viewer_ui

265:

{ void addActionListener ( ActionListener observer );

266:

void write ( String value );

267:

void write ( double value );

268:

void requestFocus ();

269: } 270: //---------------------------------------------------------------- 271:

private static

272:

class Keypad_viewer extends Calculator_keypad implements Viewer_ui

273: { 274:

public void write( String value ){/* ignore the request */}

275:

public void write( double value ){/* ignore the request */}

276: } 277: //---------------------------------------------------------------- 278:

private static class Tape_viewer extends Tape implements Viewer_ui

279: {} 280: 281: /***************************************************************** 282: * The Viewer class is just a container for the Viewer_ui 283: * object. It encapsulates the code that puts up the "UI" 284: * menu and switches Viewer_ui when asked. The Viewer_ui is 285: * put into a container so that the user of a Viewer doesn't 286: * need to know when the UI changes. 287: **/ 288:

private class Viewer extends JPanel

289: { 290:

private Menu_site menu_site = null;

291:

private Viewer_ui view = null;

292: //------------------------------------------------------------ 293:

public Viewer()

294: { addAncestorListener 295: ( new AncestorAdapter() 296:

{ public void ancestorAdded( AncestorEvent event )

297: { if( menu_site == null ) // this is first call 298: { menu_site = (Menu_site) 299: SwingUtilities.getAncestorOfClass( 300: Menu_site.class, Viewer.this); 301: use_tape(); 302: } 303: } 304: } 305: ); 306: setLayout( new BorderLayout() ); 307: } 308: //------------------------------------------------------------ 309:

public void write( String value ){ view.write(value); }

310:

public void write( double value ){ view.write(value); }

311: //------------------------------------------------------------ 312:

public void requestFocus()

313: { view.requestFocus(); 314: } 315: /*************************************************************** 316: * Installed in the new view, when one is swapped in by 317: * {@link #replaced_view_with}. 318: */ 319:

private final ActionListener mediator =

320: new ActionListener() 321:

{ public void actionPerformed(ActionEvent event)

322: { parse( event.getActionCommand() ); 323: } 324: }; 325: 326: /*************************************************************** 327: * This method is called whenever the system is asked to replace 328: * a view. It's passed the Class object for the requested view, 329: * and if an object of that class isn't being used as the current 330: * view, an object is created and installed, replacing the earlier 331: * view. Any menus installed by the previous view are also destroyed 332: * at this time. 333: * 334: * @return true if we replaced the current view with the requested one. 335: */ 336: 337:

private boolean replaced_view_with( Class requested )

338: { 339: try 340: { if( view != null ) 341: { if( view.getClass() == requested ) 342: return false; 343: 344: remove( (JComponent)view ); 345: if( menu_site != null ) 346: menu_site.remove_my_menus( view ); 347: } 348: 349: Viewer_ui new_viewer = 350: (Viewer_ui)( requested.newInstance() ); 351: 352:

new_viewer.addActionListener( mediator );

353: 354: add( (JComponent)(view = new_viewer), BorderLayout.CENTER ); 355: ((JComponent)view).requestFocus(); 356: return true; 357: } 358: catch( InstantiationException e /*newInstance*/ ) 359: { throw new Error("Internal Parser.java (1): " 360: + "Can't instantiate " 361: + requested.getName() 362: ); 363: } 364: catch( IllegalAccessException e /*newInstance*/ ) 365: { throw new Error("Internal Parser.java (2): " 366: + "Can't access " 367: + requested.getName() 368: ); 369: } 370: } 371: //------------------------------------------------------------ 372:

private final void use_tape()

373: { if( replaced_view_with(Tape_viewer.class) ) 374: { if( menu_site != null ) 375: { JMenuItem user_interface_help = 376: Menu_site.Implementation.line_item 377: ( "User-Interface Help", 378: new ActionListener() 379:

{ public void actionPerformed(ActionEvent event)

380: { popup_help(); 381: } 382: } 383: ); 384: 385: menu_site.add_line_item( view, 386: user_interface_help,"Help"); 387: } 388: force_layout(); 389: } 390: } 391: //------------------------------------------------------------ 392:

private final void use_keypad()

393: { if( replaced_view_with(Keypad_viewer.class) ) 394: { if( menu_site != null ) 395: { 396: // Bug: Putting these requests in a menu at this 397: // level is somewhat inconsistent as whatever 398: // value that's in the keypad's accumulator won't 399: // be pushed before the operation is executed. 400: 401: JMenu advanced = Menu_site.Implementation.menu( 402: "Advanced"); 403: 404: add_menu_item(advanced, POW, 405: "Raise TOS-1 to the power of TOS" ); 406: 407: add_menu_item(advanced, HMS2DEC, 408: "HH.MMSS->HH.decimal_minutes"); 409: 410: add_menu_item(advanced, INVERT, "Change sign" ); 411: add_menu_item(advanced, SQRT, "Square Root" ); 412: add_menu_item(advanced, CLEAR, "Clear Stack" ); 413: add_menu_item(advanced, DROP, "Drop" ); 414: add_menu_item(advanced, SWAP, "Swap" ); 415: 416: menu_site.add_menu( view, advanced ); 417: } 418: force_layout(); 419: } 420: } 421: /************************************************************** 422: * Cause the container that contains the current Viewer to 423: * lay itself out. 424: * 425: * The default "repaint()" is supposed to lay out the container 426: * of which the Viewer is a member, but it doesn't work. 427: * This kludge solves the problem for the time being, 428: * but it doesn't work particularly well: 429: * the screen flickers and the resulting object size 430: * is sometimes unpredictable. 431: */ 432:

private final void force_layout()

433: { 434: // Find the outermost window 435: Container current = null; 436: 437: for(Container next_level_up = this; next_level_up != null;) 438: { current = next_level_up; 439: next_level_up = current.getParent(); 440: } 441: 442: // Hide the outermost window, change its size, 443: // then make it visible again. 444: 445: current.setVisible( false ); 446: Dimension size = current.getSize(); 447: size.height += direction; 448: current.setSize( size ); 449: current.setVisible( true ); 450: 451: direction = -direction; // Go in the other direction 452: // next time you're called. 453: } 454:

private int direction = 1;

455: 456: /************************************************************* 457: * Add an item to the "Advanced" menu that's displayed when 458: * the keypad view is visible. 459: */ 460: 461:

private final void add_menu_item(JMenu menu, char command,

462: String label ) 463: 464: { JMenuItem item = 465: Menu_site.Implementation.line_item( label, menu_handler); 466: item.setName( "" + command ); 467: menu.add( item ); 468: } 469: 470: // The same menu handler object is used for all menus. 471: 472:

private final ActionListener menu_handler =

473: new ActionListener() 474:

{ public void actionPerformed(ActionEvent event)

475: { JMenuItem source = (JMenuItem)(event.getSource()); 476: parse( source.getName() ); 477: } 478: }; 479: 480: /************************************************************* 481: * Add our own items to the menu site when the current control 482: * is "realized." 483: */ 484:

public void addNotify()

485: { super.addNotify(); 486: make_menu(); 487: } 488: /************************************************************* 489: * Add items of relevance to the Parser to the containing menu 490: * site (if there is one). 491: */ 492:

public void make_menu( )

493: { if( menu_site != null ) 494: { JMenu UI_menu = 495: Menu_site.Implementation.menu("Interface"); 496: 497: UI_menu.add 498: ( Menu_site.Implementation.line_item 499: ( "Good", 500: new ActionListener() 501:

{ public void actionPerformed(ActionEvent e)

502: { use_tape(); 503: } 504: } 505: ) 506: ); 507: 508: UI_menu.add 509: ( Menu_site.Implementation.line_item 510: ( "Bad", 511: new ActionListener() 512:

{ public void actionPerformed(ActionEvent e)

513: { use_keypad(); 514: } 515: } 516: ) 517: ); 518: 519: menu_site.add_menu( this, UI_menu ); 520: } 521: } 522: /************************************************************* 523: * Get rid of the menu items added by addNotify(); 524: */ 525:

public void removeNotify()

526: { super.removeNotify(); 527: if( menu_site != null ) 528: menu_site.remove_my_menus( this ); 529: } 530: 531: //------------------------------------------------------------ 532:

public Dimension getPreferredSize()

533: { return ((JComponent)view).getPreferredSize(); 534: } 535: //------------------------------------------------------------ 536:

public Dimension getMinimumSize()

537: { return ((JComponent)view).getPreferredSize(); 538: } 539: //------------------------------------------------------------ 540:

public void doLayout()

541: { super.doLayout(); 542: ((JComponent)view).setSize( getSize() ); 543: } 544: } 545: }

Whew!

So there's a full-blown app. The RPN calculator is characteristic of object-oriented systems generally: it's more complex than an equivalent procedural system, but the complexity is organized in such a way that it's easier to manage -- all changes to the system are localized. If you need to add a view to the parser, for example, all you need to do is change one class -- the Parser. Moreover, bugs are also localized in that they tend to affect the working of only one class, and when they're fixed, the effects of the change do not ripple out to the rest of the program.

This article also finishes up the series on object-oriented UI implementation. There's a lot more to the UI topic, of course, but this stuff has been said elsewhere as well as I can say it. The main point I wanted to get across in this series was the notion of object-oriented style encapsulation and how you can build quite flexible UIs without violating that encapsulation. Secondarily, I wanted to show you how you can build applications that are modularized to the point that all changes to a particular class -- including UI changes -- can be concentrated in one place. This concentration of functionality (often called coherence) is a concept central to object-oriented design, and it's one the main reasons why object-oriented systems are easier to maintain than procedural systems. The visual-proxy pattern that I've presented not only gives you this coherence, but it is also useful in non-GUI applications as well. For example, a proxy can just as well generate an HTML form as it can an interactive UI (maybe there's a future column here). I plan to revisit user interfaces on occasion, but for now I'm on to other topics.

In the next few Java Toolboxes I plan to start looking at various topics that come under the broad category of security. I plan to look at (in no particular order) things like:

Until next time.

Allen Holub runs Holub Associates, a software-design firm based in the San Francisco Bay Area. Holub Associates is a software-architecture house. It designs software and guides you through the implementation process in much the same way that traditional architects design buildings and help contractors construct them. Holub Associates also provides training in object-oriented-design and Java, provides design-review and mentoring services, and does occasional Java implementation work. Allen has been working in the computer industry since 1979. He is widely published in magazines (Dr. Dobb's Journal, Programmers Journal, Byte, MSJ, among others). He has seven books to his credit and is working on an eighth that will present the complete sources for a Java compiler written in Java. After eight years as a C++ programmer, Allen abandoned C++ for Java in early 1996. He now looks at C++ as a bad dream, the memory of which is mercifully fading. He's built two operating systems from scratch, several compilers, and various application programs ranging in scope from robotics controllers to children's software. He's been teaching programming (first C, then C++ and MFC, now object-oriented-Design and Java) both on his own and for the University of California Berkeley Extension since 1982. Get information, and contact Allen, via his Website http://www.holub.com.

Learn more about this topic

  • The code
  • An example RPN calculator
  • Russ Webb's RPN calculator provides a good example of Reverse Polish Notation for the PalmPilot platform
    http://www.nthlab.com
  • UML resources
  • The Unified Modeling Language (UML) notation is gradually winning the notation wars. UML is an emerging standard, with the Object Management Group (OMG) in charge of the process. Get more information from OMG at
    http://www.omg.org/uml
  • UML Distilled, by Martin Fowler and Kendall Scott (Addison-Wesley, 2000), is my favorite quick introduction to UML
    http://www1.fatbrain.com/asp/BookInfo/BookInfo.asp?theisbn=020165783X&from=NCN454
  • The UML specification can be downloaded from
    http://www.rational.com/uml/resources/documentation
  • A word of warningThe foregoing link is to the Rational Web site because the principal designers of UMLGrady Booch, James Rumbaugh, and Ivor Jacobsen all work for Rational. Nonetheless, Rational Rose, their object-oriented CASE tool, is one of the weakest -- though most popular -- of the object-oriented CASE tools available. For one thing, Rose supports only a small subset of UML and doesn't do a particularly good job of it. The drawings in this article were created using Visio (which I'm also not ecstatic about) using templates of my own devising. You can get these templates from my Website in the Object-Oriented Design subsection of the Goodies section.
    http://www.holub.com.
  • Design patterns
  • Swing architecture