// Author: Magnus Edvald Bjornsson // (magnus@cs.brandeis.edu) // // Copyright 1998 // // Date: Dec 4, 1998 // // Digital Manipulative - A Simple Calculator // This application displays a GUI, which allows the user to drag // both numerals, operators and brackets onto a grid. // When pressed each component evaluates its position, numerals // give their respective decimal values and operators give the // result of their binary operation on their left and right // neighbours. // // A final project in cs113, Machine Learning at Brandeis University // // Revisions: // 6.12.1998 - Added brackets and decimal values for numerals // 10.12.1998 - Added an error message if some error is found in // the arithmatic expression // import java.awt.*; import java.awt.event.*; import java.util.*; import java.io.*; public class dmCalc extends Frame implements MouseListener, MouseMotionListener { /***************************************************** * Member variables * *****************************************************/ protected Token[][] objGrid = new Token[15][11]; // The grid protected Numeral[] numPicks = new Numeral[10]; // Possible numerals to pick protected Operator[] opPicks = new Operator[3]; // Possible operators to pick protected Bracket[] bracketPicks = new Bracket[2]; // Possible brackets to pick protected Panel gridPanel = new Panel(); // Panel which contains the grid protected Panel topPanel = new Panel(); // Panel for the numerals protected Panel leftPanel = new Panel(); // Panel for the operators protected Panel resultPanel = new Panel(); // Panel for the results protected Label resultLabel = new Label(); // The label in which the results // are displayed // After putting a new numeral/operator on the grid, we have to // place it before it gets 'activated'. protected boolean isFirst = false; // Are we working with a numeral or an operator protected boolean isNumeral = false; // The Numeral/Operator we are placing on the grid with. protected Numeral tempNumeral; protected Operator tempOperator; protected Token tempToken; protected Thread tempThread; public static void main(String[] args) { new dmCalc(); } // Creates the frame and puts all the panels and tokens in place // Also makes the tokens (components) listen for mouse events. public dmCalc() { // First we create the frame super("Digital Manipulative - Simple Calculator"); setSize(640, 480); // We set up the layout next this.setLayout( new BorderLayout() ); topPanel.setLayout( new FlowLayout(FlowLayout.CENTER) ); leftPanel.setLayout( new GridLayout(6,1) ); resultPanel.setLayout( new FlowLayout(FlowLayout.LEFT) ); topPanel.setBackground(Color.gray); leftPanel.setBackground(Color.gray); gridPanel.setBackground(Color.lightGray); // ...and then initalize the pickable objects. // First we create the numerals topPanel.add( numPicks[0] = new Numeral(new Integer(0), Color.red) ); topPanel.add( numPicks[1] = new Numeral(new Integer(1), Color.blue) ); topPanel.add( numPicks[2] = new Numeral(new Integer(2), new Color(123,231,19)) ); topPanel.add( numPicks[3] = new Numeral(new Integer(3), Color.cyan) ); topPanel.add( numPicks[4] = new Numeral(new Integer(4), Color.green) ); topPanel.add( numPicks[5] = new Numeral(new Integer(5), Color.magenta) ); topPanel.add( numPicks[6] = new Numeral(new Integer(6), Color.orange) ); topPanel.add( numPicks[7] = new Numeral(new Integer(7), Color.pink) ); topPanel.add( numPicks[8] = new Numeral(new Integer(8), Color.white) ); topPanel.add( numPicks[9] = new Numeral(new Integer(9), Color.yellow) ); // and then the operators opPicks[0] = new Operator("plus", "+"); opPicks[1] = new Operator("minus", "-"); opPicks[2] = new Operator("times", "x"); // and finally the brackets bracketPicks[0] = new Bracket("("); bracketPicks[1] = new Bracket(")"); // Let's add the result label to the result panel resultPanel.add( resultLabel ); resultLabel.setText("You start by dragging some numerals and operators on to the grid to create an arithmatic expression."); // Now we group the panels on the window frame. add("North", topPanel); add("West", leftPanel); add("Center", gridPanel); add("South", resultPanel); // If the window is closed the program should quit. this.addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent e ) { System.exit(0); } }); // Here we start listening for mouseclicks on each of the objects for (int i=0; i < opPicks.length; i++) { opPicks[i].addMouseListener( this ); opPicks[i].addMouseMotionListener( this ); leftPanel.add(opPicks[i]); } for (int i=0; i < numPicks.length; i++) { numPicks[i].addMouseListener( this ); numPicks[i].addMouseMotionListener( this ); } for (int i=0; i < bracketPicks.length; i++) { bracketPicks[i].addMouseListener( this ); bracketPicks[i].addMouseMotionListener( this ); leftPanel.add(bracketPicks[i]); } // ...and finally we display the whole shebang. this.setBackground(Color.white); this.show(); this.setResizable(false); } // Since we have enable events, we'll get all mouse events and mouse // motion events. public void mousePressed( MouseEvent e ) { Graphics g = gridPanel.getGraphics(); isFirst = true; } public void mouseClicked(MouseEvent e) {;} // If we get message that the mouse has been released we place the component // on the grid. // NOTICE: that once on the grid, the token "gobbles" up the mouse event public void mouseReleased(MouseEvent e) { int x = e.getX() / 40; int y = e.getY() / 40; tempToken.onGrid = true; tempToken.gridLocX = x; tempToken.gridLocY = y; // Something is already in our place...let's remove it if (objGrid[x][y] != null) { if (objGrid[x][y] instanceof Numeral) gridPanel.remove((Numeral)objGrid[x][y]); else if (objGrid[x][y] instanceof Bracket) gridPanel.remove((Bracket)objGrid[x][y]); else if (objGrid[x][y] instanceof Operator) gridPanel.remove((Operator)objGrid[x][y]); } // Now we insert our own object onto the grid if (tempToken instanceof Numeral) { System.out.println("Inserting Numeral object into objGrid array at: ["+x+"]["+y+"]"); objGrid[x][y] = (Numeral)tempToken; tempThread = new Thread((Numeral)tempToken); } else if (tempToken instanceof Operator) { System.out.println("Inserting Operator object into objGrid array at: ["+x+"]["+y+"]"); objGrid[x][y] = (Operator)tempToken; tempThread = new Thread((Operator)tempToken); } else if (tempToken instanceof Bracket) { System.out.println("Inserting Bracket object into objGrid array at: ["+x+"]["+y+"]"); objGrid[x][y] = (Bracket)tempToken; tempThread = new Thread((Bracket)tempToken); } // And start it running as a seperate thread tempThread.start(); gridPanel.repaint(); this.repaint(); } public void mouseEntered(MouseEvent e) {;} public void mouseExited(MouseEvent e) {;} // The token is being dragged...we simply let it follow the mouse. // NOTICE: The dragging is skewed since we are dragging between panels. public void mouseDragged(MouseEvent e) { if (isFirst) { try { tempToken = (Token)((Token)e.getSource()).clone(); } catch (CloneNotSupportedException cc) { System.out.println("Cloning of the numeral unsuccessful!"); } gridPanel.add(tempToken); tempToken.setSize(38,38); isFirst = false; } tempToken.setLocation(e.getX()-20, e.getY()-20); repaint(); } public void mouseMoved(MouseEvent e) {;} // We let the main object handle the Thread creation and placing the object. // When the object has been placed, itself takes care of the calculation. public void paint( Graphics g ) { // Let's draw a grid, which we work on, where each // grid element is 40x40 pixels. (Given that the Frame // is 640 by 480, that gives us a grid of 16 by 12. Graphics a = gridPanel.getGraphics(); for (int i=0; i < getSize().height; i=i+40) { a.drawLine(0,i,getSize().width,i); } for (int i=0; i < getSize().width; i=i+40) { a.drawLine(i,0,i,getSize().height); } } // Super class for numerals, operators and brackets abstract class Token extends Component implements Cloneable { /*********************************** * Member variables * ***********************************/ protected Point location; // Pixel location of the object protected Point arrayLocation; // Location in the objGrid protected String strOp; // "verbal" version of the operator // (if I were sometime to implement speech) protected String displayOp; // operator to display on screen protected Color letterColor; // the color of the token protected boolean isError = false; protected int gridLocX, gridLocY; // location on the grid public boolean onGrid = false; // Listener registrar protected MouseListener mouseListener; protected MouseMotionListener mouseMotionListener; // Reference to an image representation of this instance (not used) public Image image; public synchronized void addMouseListener(MouseListener l) { mouseListener = AWTEventMulticaster.add(mouseListener, l); } public synchronized void removeMouseListener(MouseListener l) { mouseListener = AWTEventMulticaster.remove(mouseListener, l); } public synchronized void addMouseMotionListener(MouseMotionListener mml) { mouseMotionListener = AWTEventMulticaster.add(mouseMotionListener, mml); } public synchronized void removeMouseMotionListener(MouseMotionListener mml) { mouseMotionListener = AWTEventMulticaster.remove(mouseMotionListener, mml); } // This fires the event to every listener. Since this simply report the mouse // location and stuff. protected void fireEvent(AWTEvent e) { MouseEvent me; if (e instanceof MouseEvent) { me = (MouseEvent)e; MouseEvent event = new MouseEvent( this, me.getID(), me.getWhen(), me.getModifiers(), me.getX(), me.getY(), me.getClickCount(), me.isPopupTrigger() ); switch(e.getID()) { case MouseEvent.MOUSE_PRESSED: if (mouseListener != null) mouseListener.mousePressed(me); break; case MouseEvent.MOUSE_RELEASED: if (mouseListener != null) mouseListener.mouseReleased(me); break; case MouseEvent.MOUSE_CLICKED: if (mouseListener != null) mouseListener.mouseClicked(me); break; case MouseEvent.MOUSE_ENTERED: if (mouseListener != null) mouseListener.mouseEntered(me); break; case MouseEvent.MOUSE_EXITED: if (mouseListener != null) mouseListener.mouseExited(me); break; case MouseEvent.MOUSE_MOVED: if (mouseMotionListener != null) mouseMotionListener.mouseMoved(me); break; case MouseEvent.MOUSE_DRAGGED: if (mouseMotionListener != null) mouseMotionListener.mouseDragged(me); break; } // end of switch } // end of if statement } // end of fireEvent method public Object clone() throws CloneNotSupportedException { Token retVal = (Token)super.clone(); return retVal; } } // Superclass for bracket and numeral (which emit information to their left // and right. class leftRightEmitter extends Token implements Cloneable { public String strLeftValue; // the value which is emitted _to the left_ public String strRightValue; // the value which is emitted _to the right_ public Object clone() throws CloneNotSupportedException { leftRightEmitter retVal = (leftRightEmitter)super.clone(); return retVal; } } class Numeral extends leftRightEmitter implements Runnable, Cloneable { /*********************************** * Member variables * ***********************************/ protected int intValue; // the value of the numeral public int decValue; // the decimal value of the numeral public Numeral(Integer val, Color col) { intValue = val.intValue(); strLeftValue = String.valueOf(intValue); strRightValue = String.valueOf(intValue); decValue = 1; letterColor = col; enableEvents( AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK); } public void paint( Graphics g ) { Font f = new Font("Helvetica", Font.PLAIN, 36); g.setFont(f); g.setColor( letterColor ); g.drawString(String.valueOf(intValue), 10, 35); } // When the numeral starts running as a thread...this is what we do: public void run() { System.out.println("Started running numeral thread"); while(true) { if (gridLocX > 0) { if (objGrid[gridLocX-1][gridLocY] instanceof Numeral) { // There is a numeral to our left, so we change the value // we emit to the right by adding our own value as a suffix // to the value we read from left. strRightValue = ((Numeral)objGrid[gridLocX-1][gridLocY]).strRightValue + intValue; } } if (gridLocX < 14) { if (objGrid[gridLocX+1][gridLocY] instanceof Numeral) { // There is a numeral to our right, so we change the value // we emit to the left by adding our own value as a prefix // to the value we read from right. // We also change our decimal value by a factor of 10. decValue = ((Numeral)objGrid[gridLocX+1][gridLocY]).decValue * 10; strLeftValue = intValue + ((Numeral)objGrid[gridLocX+1][gridLocY]).strLeftValue; } } try { Thread.sleep(100); } catch (InterruptedException e) {} } } protected void processEvent( AWTEvent e ) { if (!onGrid) { // Since the object is not on the grid, the user is pressing the // pickable objects. Therefore we let the main object handle it. fireEvent(e); } else { // If the object is on the grid, each mouse click means some specific // operation, specific to this object. if ( e.getID() == MouseEvent.MOUSE_PRESSED ) { // Since pressing a number does nothing, we simply print a short message. resultLabel.setText("My decimal value is " + (intValue * decValue) ); } } } public Object clone() throws CloneNotSupportedException { Numeral retVal = (Numeral)super.clone(); return retVal; } public Dimension getPreferredSize() { return new Dimension(40,40); } } //end of class Numeral class Operator extends Token implements Runnable, Cloneable { int result; public int leftNum; // The number to the left of the operator public int rightNum; // The number to the right of the operator public Operator(String op, String dop) { strOp = op; displayOp = dop; enableEvents( AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK); } public void paint( Graphics g ) { Font f = new Font("Helvetica", Font.PLAIN, 36); g.setFont(f); g.drawString(displayOp, 10, 30); } public void run() { System.out.println("Started running operator thread"); while(true) { if (gridLocX > 0 & gridLocX < 14) { if ( ((objGrid[gridLocX-1][gridLocY] instanceof Numeral) | (objGrid[gridLocX-1][gridLocY] instanceof Bracket)) & ((objGrid[gridLocX+1][gridLocY] instanceof Numeral) | (objGrid[gridLocX+1][gridLocY] instanceof Bracket)) ){ // If there are brackets or numerals on both side of this operator // we get their value and start calculating leftNum = Integer.parseInt(((leftRightEmitter)objGrid[gridLocX-1][gridLocY]).strRightValue); rightNum = Integer.parseInt(((leftRightEmitter)objGrid[gridLocX+1][gridLocY]).strLeftValue); isError = false; if (displayOp.equals("x")) result = leftNum * rightNum; else if (displayOp.equals("-")) result = leftNum - rightNum; else //(displayOp.equals("+")) result = leftNum + rightNum; } else // Not a valid aritmatic expression isError = true; try { Thread.sleep(100); } catch (InterruptedException e) {} } else isError = true; } } protected void processEvent( AWTEvent e ) { if (!onGrid) { fireEvent(e); } else { if ( e.getID() == MouseEvent.MOUSE_PRESSED ) { // the mouse button has been pressed if ( objGrid[gridLocX-1][gridLocY] != null & objGrid[gridLocX+1][gridLocY] != null) { // There is something both to our left and right if ( ((Token)objGrid[gridLocX-1][gridLocY]).isError | ((Token)objGrid[gridLocX+1][gridLocY]).isError ) resultLabel.setText("I detect an error in this arithmatic expression!"); else resultLabel.setText("The result of "+ leftNum +" "+strOp+" "+ rightNum +" is "+ result); } } } } public Object clone() throws CloneNotSupportedException { Operator retVal = (Operator)super.clone(); return retVal; } public Dimension getPreferredSize() { return new Dimension(40,40); } } // end of Operator class // Usage: Bracket newBracket = new Bracket("b"); // Pre: b is either ( or ) class Bracket extends leftRightEmitter implements Runnable, Cloneable { boolean isLeftBracket; String dispBracket; public Bracket(String db) { dispBracket = db; if (dispBracket.equals("(")) isLeftBracket = true; else isLeftBracket = false; strLeftValue = "0"; strRightValue = "0"; isError = false; enableEvents( AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK); } public void paint( Graphics g ) { Font f = new Font("Helvetica", Font.PLAIN, 36); g.setFont(f); g.drawString(dispBracket, 10, 30); } public void run() { while(true) { boolean opIsFound = false; int numberOfSkips = 0; // The bracket must find out what the expression they contain is, and // store that value in strLeftValue or strRightValue. if (!isLeftBracket & (gridLocX > 0)) { // We start considering right bracket. // The right bracket radiates it's value (or the value of the expression // the pair of brackets contain) to the right. // We require the location on the grid to be greater than 0 // Let's first find the nearest operator on the left side. int i = gridLocX - 1; while (i > 0) { if (objGrid[i][gridLocY] instanceof Bracket) { // If we find a bracket if ( !((Bracket)objGrid[i][gridLocY]).isLeftBracket ) // and it's a right bracket, know that we should // skip the next operator numberOfSkips++; else if ( ((Bracket)objGrid[i][gridLocY]).isLeftBracket ) // otherwise if it's a "closing" bracket // we lower the number of skips if there are some numberOfSkips--; } if (objGrid[i][gridLocY] instanceof Operator) { if (numberOfSkips == 0) { opIsFound = true; break; } } i--; } if (opIsFound) { // An operator has been found. This means that we can extract its // value and make that our own right value; strRightValue = String.valueOf(((Operator)objGrid[i][gridLocY]).result); isError = false; } else { isError = true; strRightValue = "0"; } } // Now we consider a left bracket (which radiates it's number to the left) else if (isLeftBracket & (gridLocX < 14)) { int i = gridLocX + 1; while (i < 14) { if (objGrid[i][gridLocY] instanceof Bracket) { // If we find a bracket if ( ((Bracket)objGrid[i][gridLocY]).isLeftBracket ) // and it's a also a left bracket, we increase the number of skips numberOfSkips++; else if ( !((Bracket)objGrid[i][gridLocY]).isLeftBracket ) // otherwise it's a "closing" bracket and we reduce the number of skips numberOfSkips--; } if (objGrid[i][gridLocY] instanceof Operator) { if (numberOfSkips == 0) { opIsFound = true; break; } } i++; } if (opIsFound) { // An operator has been found. This means that we can extract its // value and make that our own left value; strLeftValue = String.valueOf(((Operator)objGrid[i][gridLocY]).result); isError = false; } else { isError = true; strRightValue = "0"; } } try { Thread.sleep(100); } catch (InterruptedException e) {} } } protected void processEvent( AWTEvent e ) { if (!onGrid) { fireEvent(e); } else { if ( e.getID() == MouseEvent.MOUSE_PRESSED) { String typeOfBracket = "right"; if (isLeftBracket) typeOfBracket = "left"; resultLabel.setText("I'm a "+typeOfBracket+" bracket!"); } } } public Object clone() throws CloneNotSupportedException { Bracket retVal = (Bracket)super.clone(); return retVal; } public Dimension getPreferredSize() { return new Dimension(40,40); } } // end of Bracket class } //end of program