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

The RPN-calculator application demonstrates object-oriented UI principles

1 2 3 4 5 6 Page 6
Page 6 of 6
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:

  • How to build a secure sandbox suitable for user-supplied plug-ins
  • How to write an auto-updating class loader that will automatically keep client-side applications up-to-date
  • How to extend Java's cryptography extensions with new algorithms (using a Java Twofish implementation)

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

Related:
1 2 3 4 5 6 Page 6
Page 6 of 6