import java.awt.Canvas;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.image.ImageObserver;
import java.awt.image.MemoryImageSource;
import java.awt.image.PixelGrabber;
import java.awt.Rectangle;
import java.lang.Math;
import java.util.Vector;

/**
 * This class is an implementation of a graphics window, a Java
 * AWT Canvas.  All graphics is saved for re-display in the event
 * of resizing. Other features are:
 * 

* - the coordinate space is the traditional mathematics; [0,0] is * in the middle of the graphics window. *

* - KeyEvents; keyPressed() is captured and can be passed on. *

* - MouseEvents; mouseReleased() is captured and can be passed on. *

* - higher-level functionality (i.e., flood-fill provided). *

* - in-memory Image maintained with buffered operations to improve * graphics display performance. *

* @author Guy Haas */ public class TGCanvas extends Canvas implements KeyListener, MouseListener { // global constants // public static final int MAX_TURTLES = 12; // constants // private static final Color INITIAL_BACKGROUND = Color.white; private static final Color INITIAL_FOREGROUND = Color.black; private static final int INITIAL_FONT_SIZE = 14; private static final int INITIAL_FONT_STYLE = Font.PLAIN; private static final String INITIAL_FONT_NAME = "Courier"; private static final int INITIAL_PEN_SIZE = 2; private static final int MINIMUM_HEIGHT = 20; // variables with class-wide scope // private boolean refreshNeeded; // true if we've detected external // request for paint() e.g., TG was // covered in the background and then // brought to front private boolean showFrame; private int canvasHeight; private int canvasWidth; private int mouseX, mouseY; // position of mouse when last clicked private int xCenter, yCenter; // these AWT graphics coordinates will // be [0,0] for the TGCanvas // coordinate space. *NOTE* i tried // making these "float" but when they // had a .5 fractional part, pixel // choice by AWT was poorer. i ended // up with lines with endpoints one // pixel apart instead of straight private Graphics giGraphics; // Graphics object for graphicsImage private Image graphicsImage; // in-memory Image for the composite // graphics - all the stuff on the // display except for the turtle(s) private Rectangle[] turtleClipRect; // clipRects used to draw turtle images private Turtle[] turtles; // array of turtles that want to be // displayed private Vector graphicsOps; // a list of Graphics operations // pending processing private Vector keyHandlers; // Objects that want their keyPressed() // method invoked when our KeyListener // interface: keyPressed() method // is invoked, propagating key stuff private Vector mouseHandlers; // Objects that want their mouseClick() // method invoked when our MouseListener // interface: mouseReleased() method // is invoked, propagating mouse stuff private Vector refreshList; // a list of Graphics operations // processed since the TGCanvas object // was created or its last invocation // of clean() // // TGCanvas Constructors // public TGCanvas() { this( 700, 400 ); } public TGCanvas( int width, int height ) { this.setSize( width, height ); this.addKeyListener(this); this.addMouseListener(this); canvasWidth = width; canvasHeight = height; xCenter = canvasWidth / 2; yCenter = canvasHeight / 2; Font font = new Font( INITIAL_FONT_NAME, INITIAL_FONT_STYLE, INITIAL_FONT_SIZE ); this.setFont( font ); graphicsOps = new Vector( 200, 100 ); keyHandlers = new Vector( 5, 5 ); mouseHandlers = new Vector( 5, 5 ); refreshList = new Vector( 1000, 500 ); turtleClipRect = new Rectangle[ TGCanvas.MAX_TURTLES ]; turtles = new Turtle[ TGCanvas.MAX_TURTLES ]; refreshNeeded = false; showFrame = false; } // end TGCanvas( int, int ) // // KeyListener methods // public void keyPressed(KeyEvent ke) { int numHandlers = keyHandlers.size(); if ( numHandlers == 0 ) return; int keyCode = ke.getKeyCode(); //System.out.println( "KeyPressed: " + ke.getKeyText(keyCode) ); int tgKeyNum = 0; switch ( keyCode ) { case KeyEvent.VK_DOWN: tgKeyNum = TGKeyHandler.DOWN; break; case KeyEvent.VK_ENTER: tgKeyNum = TGKeyHandler.ENTER; break; case KeyEvent.VK_LEFT: tgKeyNum = TGKeyHandler.LEFT; break; case KeyEvent.VK_RIGHT: tgKeyNum = TGKeyHandler.RIGHT; break; case KeyEvent.VK_SPACE: tgKeyNum = TGKeyHandler.SPACE; break; case KeyEvent.VK_UP: tgKeyNum = TGKeyHandler.UP; break; } if ( tgKeyNum == 0 ) return; for (int idx=0; idx < numHandlers; idx++) { TGKeyHandler kh = (TGKeyHandler) keyHandlers.elementAt(idx); kh.keyPressed( tgKeyNum ); } } // end keyPressed() public void keyReleased(KeyEvent ke) {} public void keyTyped(KeyEvent ke) { int numHandlers = keyHandlers.size(); if ( numHandlers == 0 ) return; char ch = ke.getKeyChar(); //System.out.println( "keyTyped: " + ch + ", " + (int) ch ); //System.out.println( " " + ke.getKeyCode() ); for (int idx=0; idx < numHandlers; idx++) { TGKeyHandler kh = (TGKeyHandler) keyHandlers.elementAt(idx); kh.keyPressed( ch ); } } // end keyTyped() // MouseListener Interface Support // // I intended to only support mouseClicked(), but too many clicks // of the mouse never got to the program. Searching Sun's Java // Developer web site turned up an explanation: if the mouse is // moved at all between the press and the release, it is considered // a mouse-stroke instead of a mouse-click. i was not able to find // any details on how one could adjust a threshold differentiating // the two, so the solution is to use mouseReleased() instead of // mouseClicked() // // Currently, TGCanvas only supports capturing and propagating a // left-button release (with no modifiers, e.g. [Shift] key down). // So, if a MouseEvent is received that isn't for TGCanvas, the // AWT tree is walked and if a MouseListener is found in a parent // component - the MouseEvent is given to it. // private void fwdMouseEvent( int id, MouseEvent me ) { boolean popupTrigger = me.isPopupTrigger(); long when = me.getWhen(); int clickCount = me.getClickCount(); int modifiers = me.getModifiers(); int x = me.getX(); int y = me.getY(); Rectangle r = getBounds(); x += r.x; y += r.y; Container parent = getParent(); while ( parent != null ) { Class c = parent.getClass(); Class[] interfaces = c.getInterfaces(); for ( int i=0; i < interfaces.length; i++) { String name = interfaces[i].getName(); if ( name.equals("java.awt.event.MouseListener") ) { MouseEvent nme = new MouseEvent(parent, id, when, modifiers, x, y, clickCount, popupTrigger); switch ( id ) { case MouseEvent.MOUSE_PRESSED: ((MouseListener)parent).mousePressed( nme ); return; case MouseEvent.MOUSE_RELEASED: ((MouseListener)parent).mouseReleased( nme ); return; case MouseEvent.MOUSE_CLICKED: ((MouseListener)parent).mouseClicked( nme ); return; case MouseEvent.MOUSE_ENTERED: ((MouseListener)parent).mouseEntered( nme ); return; case MouseEvent.MOUSE_EXITED: ((MouseListener)parent).mouseExited( nme ); return; case MouseEvent.MOUSE_MOVED: ((MouseMotionListener)parent).mouseMoved( nme ); return; case MouseEvent.MOUSE_DRAGGED: ((MouseMotionListener)parent).mouseDragged( nme ); return; default: System.err.println("TGCanvas.fwdMouseEvent: bad id"); return; } } } r = parent.getBounds(); x += r.x; y += r.y; parent = parent.getParent(); } } // end fwdMouseEvent() public void mouseClicked(MouseEvent me) { fwdMouseEvent( MouseEvent.MOUSE_CLICKED, me ); } public void mouseEntered(MouseEvent me) { fwdMouseEvent( MouseEvent.MOUSE_ENTERED, me ); } public void mouseExited(MouseEvent me) { fwdMouseEvent( MouseEvent.MOUSE_EXITED, me ); } public void mousePressed(MouseEvent me) { fwdMouseEvent( MouseEvent.MOUSE_PRESSED, me ); } public void mouseReleased(MouseEvent me) { int modifiersMask = me.getModifiers(); if ( modifiersMask == InputEvent.BUTTON1_MASK ) { mouseX = me.getX(); mouseY = me.getY(); int numHandlers = mouseHandlers.size(); for (int idx=0; idx < numHandlers; idx++) { TGMouseHandler mh = (TGMouseHandler) mouseHandlers.elementAt(idx); mh.mouseClick(); } } else fwdMouseEvent( MouseEvent.MOUSE_RELEASED, me ); } // end mouseReleased() // // support methods only used in this class // private void addGraphObj( Object obj ) { synchronized (graphicsOps) { graphicsOps.addElement( obj ); } } // end addGraphObj() private void clearGraphicsImage() { if ( giGraphics != null ) { giGraphics.setClip( 0, 0, canvasWidth, canvasHeight ); giGraphics.setColor( INITIAL_BACKGROUND ); giGraphics.fillRect( 0, 0, canvasWidth, canvasHeight ); } } // end clearGraphicsImage() private void doRefresh() { synchronized ( graphicsOps ) { if ( refreshList == null ) graphicsOps.removeAllElements(); else { for (int opsCount = graphicsOps.size(); opsCount > 0; opsCount--) { Object obj = graphicsOps.firstElement(); refreshList.addElement( obj ); graphicsOps.removeElementAt( 0 ); } graphicsOps = refreshList; refreshList = new Vector( graphicsOps.size() + 200, 200 ); } if ( showFrame ) drawFrame(); else clearGraphicsImage(); } } // end doRefresh() // drawFrame paints the edge bits of the TGCanvas canvas black, // to "frame" the graphics. This must be done without using the standard // private methods, e.g. pu(), pd(), setxy(), etc... since this would // result in the frame being remembered as part of the graphics image. // the frame needs to exist outside the image so that when the graphics // canvas is resized, it can change and the older frame goes away. // private void drawFrame() { if ( giGraphics != null ) { clearGraphicsImage(); giGraphics.setColor( Color.black ); giGraphics.drawRect( 0, 0, canvasWidth-1, canvasHeight-1 ); } } // end drawFrame() private void initGraphicsImage() { graphicsImage = createImage( canvasWidth, canvasHeight ); giGraphics = graphicsImage.getGraphics(); if ( showFrame ) drawFrame(); else clearGraphicsImage(); giGraphics.setColor( INITIAL_FOREGROUND ); } // end initGraphicsImage() private Rectangle renderGraphics() { Rectangle clipRect; int leftX = canvasWidth; int rightX = -1; int upperY = canvasHeight; int lowerY = -1; if ( graphicsImage == null ) { initGraphicsImage(); leftX = 0; rightX = canvasWidth; upperY = 0; lowerY = canvasHeight; } synchronized ( graphicsOps ) { for (int opsCount = graphicsOps.size(); opsCount > 0; opsCount--) { TGGraphicsOp op = (TGGraphicsOp) graphicsOps.firstElement(); clipRect = op.doIt( graphicsImage ); if ( clipRect != null ) { if ( clipRect.x < leftX ) leftX = clipRect.x; if ( clipRect.y < upperY ) upperY = clipRect.y; int coord = clipRect.x + clipRect.width - 1; if ( coord > rightX ) rightX = coord; coord = clipRect.y + clipRect.height - 1; if ( coord > lowerY ) lowerY = coord; } if ( refreshList != null ) refreshList.addElement( op ); graphicsOps.removeElementAt( 0 ); } } int height = (lowerY + 1) - upperY; int width = (rightX + 1) - leftX; if ( height > 0 && width > 0 ) return new Rectangle( leftX, upperY, width, height ); return null; } // end renderGraphics() // // Overridden Canvas/Component Methods // public Dimension getMinimumSize() { return new Dimension ( canvasWidth, MINIMUM_HEIGHT ); } // end getMinimumSize() // either TGGraphicsOps have been queued to be performed or the // AWT has decided we need to redraw at least some part of the // Canvas, e.g., it was partially covered by some other window // that has moved/gone away. // public void paint(Graphics g) { Rectangle rect = g.getClipBounds(); //System.out.println(" TGCanvas.paint: clipRect=" + rect ); if (rect.x != 0 || rect.y != 0 || rect.height != 1 || rect.width != 1) { g.setClip( 0, 0, canvasWidth, canvasHeight ); g.setColor( INITIAL_BACKGROUND ); g.fillRect( 0, 0, canvasWidth, canvasHeight ); doRefresh(); } rect = renderGraphics(); if ( rect != null ) { g.setClip( rect ); g.drawImage( graphicsImage, 0, 0, this ); } for ( int i=0; i < turtleClipRect.length; i++ ) if ( (rect = turtleClipRect[i]) != null ) { g.setClip( rect ); g.drawImage( graphicsImage, 0, 0, this ); turtleClipRect[i] = null; } for (int i=0; i < turtles.length; i++) { Turtle turtle = turtles[i]; if ( turtle != null ) { int turtleX = (int) Math.rint( turtle.xcor() + xCenter ); int turtleY = (int) Math.rint( yCenter - turtle.ycor() ); int imgSize = turtle.getImageSideSize(); int imgLeftX = turtleX - imgSize/2; int imgTopY = turtleY - imgSize/2; g.setClip( imgLeftX, imgTopY, imgSize, imgSize); g.drawImage ( turtle.getImage(), imgLeftX, imgTopY, this ); turtleClipRect[i] = new Rectangle( imgLeftX, imgTopY, imgSize, imgSize ); } } } //end paint() // override update() to eliminate its invocation of clear() // public void update(Graphics g) { paint(g); } // ------------------------------------------------------- // methods available outside the class - exposed interface // ------------------------------------------------------- public void addKeyHandler( TGKeyHandler kh ) { if ( ! keyHandlers.contains(kh) ) keyHandlers.addElement( kh ); } // end addKeyHandler public void addMouseHandler( TGMouseHandler mh ) { if ( ! mouseHandlers.contains(mh) ) mouseHandlers.addElement( mh ); } // end addMouseHandler() public void addTurtle( Turtle turtle ) { int openIdx = -1; for (int i=turtles.length-1; i >= 0; i--) { if ( turtles[i] != null ) { if ( turtles[i] == turtle ) return; } else openIdx = i; } if ( openIdx < 0 ) { System.err.println("TGCanvas.addTurtle: no room!"); return; } turtles[openIdx] = turtle; } // end addTurtle() public int canvasHeight() { return canvasHeight; } public int canvasWidth() { return canvasWidth; } /** * Clean graphics off of the display. */ public void clean() { synchronized ( graphicsOps ) { graphicsOps.removeAllElements(); if ( refreshList != null ) refreshList.removeAllElements(); } if ( showFrame ) drawFrame(); else clearGraphicsImage(); repaint(); } // end clean() /** * Turn on the collection of graphics operations. * Saving all requested graphical operations is * needed for reconstructing screen contents. */ public void collectGraphics() { refreshList = new Vector( 1000, 500 ); } // end collectGraphics() public int colorunder( TGPoint curXY ) { String me = "TGCanvas.colorunder: "; int canvasX = curXY.canvasX( canvasWidth ); if ( canvasX < 0 || canvasX > canvasWidth ) return INITIAL_BACKGROUND.getRGB() & 0xffffff; int canvasY = curXY.canvasY( canvasHeight ); if ( canvasY < 0 || canvasY > canvasHeight ) return INITIAL_BACKGROUND.getRGB() & 0xffffff; int[] pixel = new int[1]; if ( graphicsImage == null ) return INITIAL_BACKGROUND.getRGB() & 0xffffff; PixelGrabber pg = new PixelGrabber( graphicsImage, canvasX, canvasY, 1, 1, pixel, 0, canvasWidth ); try { pg.grabPixels(); } catch (InterruptedException e) { System.err.println( me + "grabPixels interrupted" ); return INITIAL_BACKGROUND.getRGB() & 0xffffff; } int status = pg.getStatus(); //if ( (status & ImageObserver.ALLBITS ) == 0) //{ // System.err.print( me + "grabPixels ALLBITS not set" ); // System.err.println( ", status=" + status ); // //return INITIAL_BACKGROUND.getRGB() & 0xffffff; //} return pixel[0] & 0xFFFFFF; } // end colorunder() public TGPoint drawLine(TGPoint p1, double steps, double hd, int wd, Color cl ) { TGPoint p2 = p1.otherEndPoint( hd, steps ); addGraphObj( new TGLineOp(p1, p2, cl, wd) ); return p2; } public void drawLine( TGPoint p1, TGPoint p2, int wd, Color cl ) { addGraphObj( new TGLineOp(p1, p2, cl, wd) ); } public void fill( TGPoint point, Color color ) { addGraphObj( new TGFillOp(point, color) ); } public void label( String text, TGPoint p, Font font, Color color ) { addGraphObj( new TGLabelOp(text, p, font, color) ); } /** * Return a TurtleSpace x-coordinate of last mouse click *

* @see #mousey */ public int mousex() { return mouseX - xCenter; } /** * Return a TurtleSpace y-coordinate of last mouse click *

* @see #mousex */ public int mousey() { return -(mouseY - yCenter); } /** * Turn off the collection of graphics operations. * Certain applications of graphics, e.g., animation, * do not need all the graphics operations they * perform saved (needed for reconstructing screen * contents). */ public void noCollectGraphics() { refreshList = null; } // end noCollectGraphics() public void removeKeyHandler( TGKeyHandler kh ) { keyHandlers.removeElement( kh ); } public void removeMouseHandler( TGMouseHandler mh ) { mouseHandlers.removeElement( mh ); } public void removeTurtle( Turtle turtle ) { for (int i=turtles.length-1; i >= 0; i--) if ( turtles[i] == turtle ) { turtles[i] = null; return; } System.err.println("TGCanvas.removeTurtle: turtle missing!"); } // end removeTurtle() public void resizeCanvas( int width, int height ) { canvasWidth = width; canvasHeight = height; xCenter = width / 2; yCenter = height / 2; doRefresh(); graphicsImage = null; tgDoRepaint(); } // end resizeCanvas() /** * Outline the perimeter of the graphics Canvas *

* Demarcating the drawing area is nice when TGCanvas * is being used with an applet where there's no window frame. */ public void showFrame() { showFrame = true; drawFrame(); } // end showFrame() // since java has no events to warn that a closed window or // covered Component is now visible and needs refreshed, i // must find someway to distinguish these situations. i'm // choosing to provide a ?hopefully unique? clipRect so that // invocations to repaint() other than those internal to TG // will be recognized and can thus result in a full refresh. // public void tgDoRepaint() { repaint( 0, 0, 1, 1 ); } } // end class TGCanvas