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:
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:
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:
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:
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: }