Difference between revisions of "CSC111 Event-Driven Programming with Graphics111.py 2014"

From dftwiki3
Jump to: navigation, search
(Source Code: graphics111.py)
(Introduction)
Line 9: Line 9:
 
<br />
 
<br />
 
This program is based on Horstmann and Necaise's graphics library.  The new version sports several new additions:
 
This program is based on Horstmann and Necaise's graphics library.  The new version sports several new additions:
* A Circle class that is specified by its center coordinates, x and y, its radius, and its color
+
* A Circle class that is specified by its center coordinates, x and y, its diameter, and its color
* Wheel class that is made of two concentric circles, the outer one black, the inner one grey.  The wheel object is specified by its center coordinates, and its outer radius
+
* Wheel class that is made of two concentric circles, the outer one black, the inner one grey.  The wheel object is specified by its center coordinates, and its outer diameter.
 
* A Rectangle class that is specified by the coordinates of its top-left corner, its width, height, and color.
 
* A Rectangle class that is specified by the coordinates of its top-left corner, its width, height, and color.
 
* A Polygon class that is specified by a list of x,y coordinates (at least 3 pairs of coordinates), and its color.
 
* A Polygon class that is specified by a list of x,y coordinates (at least 3 pairs of coordinates), and its color.
Line 95: Line 95:
 
</source>
 
</source>
 
<br />
 
<br />
 +
 
=Source Code: graphics111.py=
 
=Source Code: graphics111.py=
 
<br />
 
<br />

Revision as of 08:58, 20 April 2014

--D. Thiebaut (talk) 08:41, 20 April 2014 (EDT)






Introduction


This program is based on Horstmann and Necaise's graphics library. The new version sports several new additions:

  • A Circle class that is specified by its center coordinates, x and y, its diameter, and its color
  • Wheel class that is made of two concentric circles, the outer one black, the inner one grey. The wheel object is specified by its center coordinates, and its outer diameter.
  • A Rectangle class that is specified by the coordinates of its top-left corner, its width, height, and color.
  • A Polygon class that is specified by a list of x,y coordinates (at least 3 pairs of coordinates), and its color.
Example:


from graphics111 import Polygon

def main():
    global menu 
    
    win = GraphicsWindow(MAXWIDTH, MAXHEIGHT)
    canvas = win.canvas()

    p1 = Polygon( (100, 100, 150, 50, 350, 50, 400, 100), 
                  (255, 0, 0 ) )
    p1.draw( canvas )

    win.wait()
    win.close()


  • A Menu class that provides 4 basic buttons: a plus symbol, a minus symbol, a left-arrow symbol, and a right arrow symbol. For the menu to work, one must setup a call-back function (see below):


Example


def main():
    global menu 
    
    win = GraphicsWindow(MAXWIDTH, MAXHEIGHT)
    canvas = win.canvas()
    
    canvas.setCallbackFunction( mouseEvent )    
    canvas.setBackground( 0, 250, 250 )

    menu = Menu(  )
    menu.draw( canvas )

    win.wait()
    win.close()


  • A call-back functionality. The main graphics library can be made aware of events such as mouse clicks, and we can define our own function that should be called when the mouse is clicked over the canvas. This is what a call-back function is. This way we can have a function that can modify the canvas in some way when the mouse is clicked at different locations on the canvas.
Example


def mouseEvent( win, canvas, x, y ):
    global menu
    
    # has a menu button been clicked?
    # if so define an action for each one

    button = menu.buttonClicked( x, y )

    if button == "LeftArrow":
        print( "left-arrow clicked!" )
        return
    if button == "RightArrow":
        print( "right-arrow clicked!" )
        return
    if button == "Minus":
        print( "minus clicked!" )
        return
    if button == "Plus":
        print( "plus clicked!" )
        return

def main():
    global menu 
    
    win = GraphicsWindow(MAXWIDTH, MAXHEIGHT)
    canvas = win.canvas()
    
    canvas.setCallbackFunction( mouseEvent )    
    canvas.setBackground( 0, 250, 250 )

    menu = Menu(  )
    menu.draw( canvas )

    win.wait()
    win.close()


Source Code: graphics111.py


# graphics111.py based on graphics.py 
# (c) 2013 by Rance Necaise 
# http://graphics.necaiseweb.org 
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions: 
#  
# The above copyright notice and this permission notice shall be included in all 
# copies or substantial portions of the Software. 
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#

## graphics.py
#  version 1.0
#  This module is part of the PyWare open source project that provides modules and 
#  and tools for use in the classroom. This module provides classes for creating 
#  top-level GUI windows that can be used for creating and displaying simple 
#  geometric shapes and color digital images. Complete documentation of the module 
#  is available at http://graphics.necaseweb.org.

## Version 1.1
# modified by D. Thiebaut to allow better use of drawPoly()
#
## Version 1.2
# modified by D. Thiebaut to allow resizing of font.
# replace font_opts by text_opts everywhere and commented out
# replacement of size by original size in setFont

import tkinter as tk
import time

## This class defines a basic top level window that can be opened on the 
#  desktop and used to produce simple graphical drawings. It contains a
#  canvas on which geometric shapes can be drawn and manipulated. 
#
class GraphicsWindow :  
  ## Creates a new graphics window with an empty canvas.
  #  @param width The horizontal size of the canvas in pixels.
  #  @param height The vertical size of the canvas in pixels.
  #    
  def __init__(self, width = 400, height = 400) :  
    global TheMainWindow
    
     # Is the window open or closed. It must be open to use most operations.
    self._isClosed = False
    
     # If this is the first toplevel window, remember it as the main window. The
     # event loop is only terminated when the main window is closed.
    if TheMainWindow is None :
      TheMainWindow = self         
    
     # Create a top-level window for the graphics window.
    self._tkwin = tk.Toplevel(rootWin, padx=0, pady=0, bd=0)
    self._tkwin.protocol("WM_DELETE_WINDOW", self.close)
    self._tkwin.title("")
      
     # Create a canvas inside the top-level window which is used for 
     # drawing the graphical objects and text.
    self._canvas = GraphicsCanvas( self, width, height ) 
    
     # Bring the window to the front of all other windows and force an update.
    self._tkwin.lift()
    self._tkwin.resizable(0, 0)    
    self._tkwin.update_idletasks()
        
  ## Closes and destroys the window. A closed window can not be used.
  # 
  def close(self) :    
     # Set the closed flag to true so no other ops can be performed on
     # the window object.
    if self._isClosed : return
    self._isClosed = True
    
     # Destroy the window and force an update so it will close when  
     # used in IDLE or Wing IDE.
    self._tkwin.destroy()
    self._tkwin.update_idletasks()
    
     # If the main window is being closed, then the mainloop has to be terminated.
    if self is TheMainWindow :
       self._tkwin.quit()
   
  ## Starts the event loop which handles various window events. This causes
  #  the sequential execution of the program to stop and wait for the user 
  #  to click the close button on the main window or to call the quit method
  #  on any window. This method should only be called on the main window.
  #  
  def wait(self) :
    if not self._isClosed :
      self._tkwin.mainloop()
 
  ## Returns a reference to the canvas contained within the window. 
  #  The canvas can be used to draw and manipulate geometric shapes and text.
  #  @return A reference to the GraphicsCanvas contained in the window.
  #
  def canvas(self) :
    if self._isClosed : raise GraphicsWinError()
    return self._canvas

  ## Sets the title of the window. By default, the window has no title.
  #  @param title A text string to which the title of the window is set. To
  #          remove the title, use pass an empty string to the method.
  #
  def setTitle(self, title) :
    self._tkwin.title( title )
    
  ## Returns a Boolean indicating whether the window exists or was previously closed. 
  #  Window operations can not be performed on a closed window.
  #  @return @c True if the window is closed and @c False otherwise.
  #
  def isClosed(self) :
    return self._isClosed
    
  ## Hides or iconifies the top level window. 
  #  The window still exists and can be displayed again using the show method.
  #
  def hide(self) :
    if self._isClosed : raise GraphicsWinError()
    self._tkwin.withdraw()
    self._tkwin.update_idletasks()
      
  ## Shows or deiconifies a previously hidden top level window.
  #
  def show(self) :
    if self._isClosed : raise GraphicsWinError()
    self._tkwin.deiconify()
    self._tkwin.update_idletasks()
  
  
#--- The graphics canvas.

## This class defines a canvas on which geometric shapes and text can be 
#  drawn. The canvas uses discrete Cartesian coordinates >= 0 with (0,0) 
#  being in the upper-left corner of the window. Unlike a canvas that a 
#  painter might use, shapes drawn on a graphics canvas are stored as 
#  objects that can later be reconfigured without having to redraw them. 
#  A collection of shape properties are also maintained as part of the 
#  canvas. These properties, which can be changed by calling specific 
#  methods, are used in drawing the various shapes and text. All shapes 
#  and text are drawn using the current context or the property settings 
#  at the time the shape is first drawn. 
#
class GraphicsCanvas :
  ## Creates a new empty graphics canvas. A graphics canvas is 
  #  automatically created as part of a GraphicsWindow. Thus, there should 
  #  be no need for the user of this module to explicitly create one.
  #  @param win, A reference to the GraphicsWindow in which the canvas is used.
  #  @param width, (int) The width of the canvas in pixels.
  #  @param height, (int) The height of the canvas in pixels.
  #   
  def __init__(self, win, width, height) :     
    # The GraphicsWindow that contains the canvas.
   self._win = win
   
    # Keep track of the size of the canvas.
   self._width = width
   self._height = height
       
    # Maintain the options used for drawing objects and text.
   self._polyOpts = {"outline" : "black", "width" : 1, "dash" : None, "fill" : ""}    
   self._textOpts = {"text" : "", "justify" : tk.LEFT, "anchor" : tk.NW,
                     "fill" : "black",
                     "font" : ("helvetica", 10, "normal") }

    # Create the tk canvas inside the given window.
   self._tkcanvas = tk.Canvas(self._win._tkwin, highlightthickness = 0, 
                              width = width, height = height, bg = "white" )
   self._tkcanvas.pack()   
  

         
   # Addition DFT 
   # set up a mouse event to capture mouse clicks on the window
   self._tkcanvas.bind("<Button-1>", self._click)
   self._tkcanvas.pack()
   self._callbackFunction = None
                       
  ## click (Addition DFT)
  # mouse-event handler for Button1
  def _click( self, event ):
    #print( "Clicked at: ", event.x, event.y )
    self._mouseX = event.x
    self._mouseY = event.y
    if self._callbackFunction != None:
      self._callbackFunction( self._win, self, self._mouseX, self._mouseY )

  ## callBack (Addition DFT)
  def setCallbackFunction( self, func ):
    self._callbackFunction = func
    
  ## getMouse (Addition DFT)
  # wait until user clicks window
  def getMouse(self ):
    self._tkcanvas.update_idletasks()     # flush any prior clicks
    self._mouseX = None
    self._mouseY = None
    while self._mouseX == None or self._mouseY == None:
        #self.update()
        self._tkcanvas.update_idletasks()
        self._checkValid()
        time.sleep(.1) # give up thread
    x, y = self._mouseX, self._mouseY
    self._mouseX = None
    self._mouseY = None
    return x,y
  
  def checkMouse( self ):
    self._tkcanvas.update_idletasks()      # flush any prior clicks
    self._mouseX = None
    self._mouseY = None
    if self._mouseX == None or self._mouseY == None:
       x, y = self._mouseX, self._mouseY
       self._mouseX = None
       self._mouseY = None
       return x,y
    else:
       return None, None
      
  ## Changes the height of the canvas. 
  #  The window is resized to fit the size of the canvas.
  #  @param size (int) The new height of the canvas in number of pixels.
  #
  def setHeight(self, size) :
    self._checkValid()
    if type(size) != int or size <= 0 :
      raise GraphicsParamError( "The window height must be >= 1." )
    self._tkcanvas.config(height=size)
    self._height = size
    self._tkcanvas.update_idletasks()

  ## Changes the width of the canvas. 
  #  The window is resized to fit the size of the canvas.
  #  @param size (int) The new width of the canvas in number of pixels.
  #
  def setWidth(self, size) :
    self._checkValid()
    if type(size) != int or size <= 0 :
      raise GraphicsParamError( "The window width must be >= 1." )
    self._tkcanvas.config(width=size)
    self._width = size
    self._tkcanvas.update_idletasks()
    
  ## Returns the height of the canvas.
  #  @return The canvas height in number of pixels.
  #
  def height( self ):
    return self._height
  
  ## Returns the width of the canvas.
  #  @return The canva width in number of pixels.
  #
  def width( self ):
    return self._width
     
  ## Clears the canvas by removing all items previously drawn on it. The canvas
  #  acts as a container of shapes and text. Thus, when a geometric shape or 
  #  text is drawn on the canvas, it is maintained internally as an object 
  #  until cleared.
  #
  def clear(self):
    self._checkValid()
    self._tkcanvas.delete(tk.ALL) 
    self._tkcanvas.update_idletasks()
   
  ## Sets the current background color of the canvas. The color can either
  #  be specified as a string that names a color or as three integer values
  #  in the range [0..255].
  #
  #     c.setBackground(colorname)
  #     c.setBackground(red, green, blue)
  #
  def setBackground(self, red, green = None, blue = None) :
    if type(red) == int :
      color = "#%02X%02X%02X" % (red, green, blue) 
    elif type(red) != str :
      raise GraphicsParamError( "Invalid color." )
    else :
      color = red
    self._checkValid()
    self._tkcanvas.config(bg = color)
    self._tkcanvas.update_idletasks()
    
  ## Sets the fill color used when drawing new polygon shapes. The color
  #  can be specified either as a string that names the color or as three
  #  integer values in the range [0..255]. If no argument is provided, it
  #  clears the fill color and the shapes will be drawn in outline form only.
  #
  #     c.setFill()
  #     c.setFill(colorname)
  #     c.setFill(red, green, blue)
  #
  def setFill( self, red = None, green = None, blue = None) :
    if red is None :
      color = ""
    elif type(red) == int :
      color = "#%02X%02X%02X" % (red, green, blue)       
    elif type(red) != str :
      raise GraphicsParamError( "Invalid color." )
    else :
      color = red
    self._polyOpts["fill"] = color
        
  ## Sets the outline color used when drawing new polygon shapes and the
  #  color used to draw lines, pixels, and text. The color can be specified 
  #  either as a string that names the color or as three integer values in 
  #  the range [0..255]. If no argument is provided, it clears the outline
  #  color. A cleared outline color is only meant for drawing polygon type 
  #  shapes that are only filled, without an outline.
  #
  #     c.setOutline()
  #     c.setOutline(colorname)
  #     c.setOutline(red, green, blue)
  #
  def setOutline( self, red = None, green = None, blue = None) :
    if red is None :
      color = ""
    elif type(red) == int :
      color = "#%02X%02X%02X" % (red, green, blue)  
    elif type(red) != str :
      raise GraphicsParamError( "Invalid color." )
    else :
      color = red
    self._polyOpts["outline"] = color
    self._textOpts["fill"] = color
     
  ## Sets both the fill and outline colors used when drawing shapes and text
  #  on the canvas. The color can be specified either as a string that names 
  #  the color or as three integer values in the range [0..255]. 
  #
  #     c.setFill(colorname)
  #     c.setFill(red, green, blue)
  #
  def setColor( self, red, green = None, blue = None ) :
    if type(red)  == int :
       color = "#%02X%02X%02X" % (red, green, blue)
    elif type(red) != str :
       raise GraphicsParamError( "Invalid color." )
    else :
       color = red
    self._polyOpts["outline"] = color
    self._polyOpts["fill"] = color
    self._textOpts["fill"] = color     
    
  ## Sets the width of lines drawn on the canvas. This includes the line and
  #  vector shapes and the outlines of polygons.
  #  @param size (int) The new line width in number of pixels.
  #
  def setLineWidth(self, size) :
    if type(size) != int or size <= 0 :
      raise GraphicsParamError( "Invalid line width." )
    self._polyOpts["width"] = size

  ## Sets the style used to drawn lines on the canvas. This includes the line
  #  and vector shapes and the outlines of polygons. 
  #  @param style (str) The style to use for new lines. It can be either 
  #               "solid" or "dashed".
  #
  def setLineStyle(self, style) :
    if style == "solid" :
      self._polyOpts["dash"] = None
    elif style == "dashed" :
      self._polyOpts["dash"] = (4,)
    else :
      raise GraphicsParamError("Invalid line style.")
      
  ## Sets the font used to draw text on the canvas. 
  #  @param family (str) The font family. It can be one of: 
  #            "arial", "courier", "times", "helvetica".
  #  @param size (int) The point size of the font.
  #  @param style (string) The font style. It can be one of:
  #            "normal", "bold", "italic", or "bold italic".
  #
  def setTextFont(self, family = None, size = None,
                   style = None) :
    origFamily, origSize, origStyle = self._textOpts["font"]
    if (family is not None and 
       family not in ('helvetica', 'arial', 'courier', 'times', 'times roman')) :
      raise GraphicsParamError( "Invalid font family." )
    else :
      family = origFamily
      
    if (style is not None and 
       style not in ('bold', 'normal', 'italic', 'bold italic')) :
      raise GraphicsParamError( "Invalid font style." )
    else :
      style = origStyle
      
    if size is not None and (type(size) != int or size <= 0) :
      raise GraphicsParamError( "Invalid font size." )
    #else :
    #   size = origSize
       
    self._textOpts["font"] = (family, size, style)           

  ## Sets the position that text is drawn in relation to a bounding box. 
  #  The (x, y) coordinate provided with drawText() is anchored to a spot on
  #  the bounding box that surrounds the text and the text is positioned 
  #  relative to the anchor. 
  #  @param position A string indicating the anchor position on the 
  #              bounding box. It can be one of:
  #              "n", "s", "e", "w", "center", "nw", "ne", "sw", "se".
  #
  def setTextAnchor(self, position) :
    if position not in ('n', 's', 'e', 'w', 'nw', 'ne', 'sw', 'se', 'center') :
      raise GraphicsParamError( "Invalid anchor position." )       
    self._textOpts["anchor"] = position
     
  ## Sets the justification used to draw new multiline text on the canvas..
  #  @param style A string specifying the justification. It can be one of:
  #               "left", "center", or "right".
  #
  def setTextJustify(self, style) :
    if style == "left" :
      self._textOpts["justify"] = tk.LEFT
    elif style == "center" :
      self._textOpts["justify"] = tk.CENTER
    elif style == "right" :
      self._textOpts["justify"] = tk.RIGHT
    else :
      raise GraphicsParamError( "Invalid justification value." )

 #--- The shape drawing methods.

  ## Draws or plots a single point (pixel) on the canvas.
  #  @param x, y  Integers indicating the (x, y) pixel coordinates at which
  #               the point is drawn.
  #  @return An integer that uniquely identifies the new canvas item.
  #
  def drawPoint( self, x, y ):
    self._checkValid()
    obj = self._tkcanvas.create_line( x, y, x+1, y,
                                    fill=self._polyOpts["outline"], 
                                    width=self._polyOpts["width"] )
    self._tkcanvas.update_idletasks()
    return obj    

  ## Draws a line segment on the canvas. The line is drawn between two 
  #  discrete end points.
  #  @param x1, y1 The coordinates of the starting point.
  #  @param x2, y2 The coordinates of the ending point.
  #  @return An integer that uniquely identifies the new canvas item.
  #
  def drawLine(self, x1, y1, x2, y2) :
    self._checkValid()
    obj = self._tkcanvas.create_line( x1, y1,
                                     x2, y2,
                                     fill=self._polyOpts["outline"], 
                                     width=self._polyOpts["width"],
                                     dash=self._polyOpts["dash"] )
    self._tkcanvas.update_idletasks()
    return obj
  
  ## Draws an arrow or vector on the canvas. The same as a line segment, 
  #  except an arrow head is drawn at the end of the segment.
  #  @returns An integer that uniquely identifies the new canvas item.
  #
  def drawArrow(self, x1, y1, x2, y2) :
    self._checkValid()
    obj = self._tkcanvas.create_line( x1, y1, x2, y2, 
                                     fill=self._polyOpts["outline"], 
                                     width=self._polyOpts["width"],
                                     dash=self._polyOpts["dash"],
                                     arrow=tk.LAST )
    self._tkcanvas.update_idletasks()
    return obj
   
  ## Draws a rectangle on the canvas. The rectangle is defined by the coordinates
  #  of the upper left corner of the rectangle and its width and height.
  #  @param x, y The coordinates of the upper-left corner of the rectangle.
  #  @param width, height The dimensions of the rectangle.
  #  @returns An integer that uniquely identifies the new canvas item.
  #
  def drawRectangle(self, x, y, width, height) :
    self._checkValid()
    obj = self._tkcanvas.create_rectangle(x, y, x + width, y + height, self._polyOpts )
    self._tkcanvas.update_idletasks()
    return obj
  
  ## The same as drawRectangle(). 
  #
  def drawRect(self, x, y, width, height) :
    return self.drawRectangle(x, y, width, height)
    
  ## Draws a polygon on the canvas. The polygon is defined by three or more vertices
  #  specified in counter-clockwise order. There are two forms of the method: 
  #  
  #     c.drawPolygon(x1, y1, x2, y2, ..., xN, yN)
  #     c.drawPolygon(sequence)
  #     
  #  @returns An integer that uniquely identifies the new canvas item.
  #  
  def drawPolygon( self, *coords ):
     # Unwrap the cooridinates which allows the method to accept individual vertices
     # or a list of vertices.
    #print( "drawPolygon: coords = ", coords )
    if len(coords) == 1 and (type(coords[0]) == list or type(coords[0] == tuple)) :       
       expCoords = tuple(*coords)
    else :
       expCoords = coords
       
    #print( "expCoords = ", expCoords )
    
    self._checkValid()
    if len(expCoords) < 6 :
      raise GraphicsParamError( "At least 3 vertices must be provided." )
    obj = self._tkcanvas.create_polygon( expCoords, self._polyOpts )
    self._tkcanvas.update_idletasks()
    return obj
  
  ## The same as drawPolygon().
  #
  def drawPoly(self, *coords) :
    # code added by DFT, April 2014
    if len(coords) == 1 and (type(coords[0]) == list
                             or type(coords[0] == tuple)) :       
       expCoords = tuple(*coords)
    else :
       expCoords = coords
    #print( "drawPoly: coords = ", coords )
    return self.drawPolygon(coords)
    
  ## Draws an oval on the canvas. The oval is defined by a bounding rectangle
  #  that is specified by the coordinates of its upper-left corner and its 
  #  dimensions. 
  #  @param x, y The upper-left coordinates of the bounding rectangle.
  #  @param width, height The dimensions of the bounding rectangle.
  #  @returns An integer that uniquely identifies the new canvas item.
  #
  def drawOval( self, x, y, width, height ):
    self._checkValid()
    obj = self._tkcanvas.create_oval( x, y, x + width, y + height, self._polyOpts )
    self._tkcanvas.update_idletasks()
    return obj    
  
    
  ## Draws an arc or part of a circle on the canvas. The arc is defined by a 
  #  bounding square and two angles. The angles are specified in degrees with 
  #  zero degrees corresponding to the x-axis.
  #  @param x, y The upper-left coordinates of the bounding square.
  #  @param diameter The dimensions of the bounding rectangle.
  #  @param startAngle The angle in degrees at which the arc begins. 
  #  @param extent The extent of the arc given as an angle in degrees. 
  #  @returns An integer that uniquely identifies the new canvas item.
  #
  def drawArc( self, x, y, diameter, startAngle, extent ):
    self._checkValid()
    obj = self._tkcanvas.create_arc( x, y, x + diameter, y + diameter, self._polyOpts,
                                    start=startAngle, extent=extent )
    self._tkcanvas.update_idletasks()
    return obj
  
  ## Draws text on the canvas. The text is drawn such that an anchor point on a
  #  bounding box is positioned at a given point on the canvas. The default 
  #  position of the anchor is in the upper-left (northwest) corner of the
  #  bounding box. The anchor position can be changed using the setTextAnchor()
  #  method. The text is drawn using the default font family, size, and style.
  #  The setTextFont() method can be used to change those characteristics. The 
  #  text to be drawn can consists of multiple lines, each separated by a
  #  newline character. The justification of the text can be set when drawing
  #  multiple lines of text.
  #  @param x, y The position on the canvas at which the anchor point of the 
  #              bounding box is positioned.
  #  @param text A string containing the text to be drawn on the canvas.
  #
  def drawText(self, x, y, text) :
    self._checkValid()
    self._textOpts["text"] = text
    obj = self._tkcanvas.create_text( x, y, self._textOpts )
    self._tkcanvas.update_idletasks()
    return obj
    
 #--- Methods that can be used to manipulate the item previously drawn on the
 #--- canvas. Each drawing method returns a unique id number used to identify 
 #--- the resulting shape. See the online documentation for more information. 
 
  def shiftItem(self, itemId, dx, dy) :
    self._checkContains(itemId)
    self._tkcanvas.move(itemId, dx, dy)    
    self._tkcanvas.update_idletasks()
  
  def removeItem(self, itemId) :
    self._checkContains(itemId)
    self._tkcanvas.delete(itemId)
    self._tkcanvas.update_idletasks()
    
  def showItem(self, itemId) :
    self._checkContains(itemId)
    self._tkcanvas.itemconfig(itemId, state = "normal")
    self._tkcanvas.update_idletasks()
    
  def hideItem(self, itemId) :
    self._checkContains(itemId)
    self._tkcanvas.itemconfig(itemId, state = "hidden")
    self._tkcanvas.update_idletasks()
    
  def raiseItem(self, itemId, above = None) :    
    self._checkContains(itemId)
    self._tkcanvas.tag_raise(itemId)   
    self._tkcanvas.update_idletasks()
  
  def lowerItem(self, itemId, below = None) :
    self._checkContains(itemId)
    self._tkcanvas.tag_lower(itemId)
    self._tkcanvas.update_idletasks()
 
  def __contains__(self, itemId):
    if self._tkcanvas.winfo_ismapped() :
      return len(self._tkcanvas.find_withtag(itemId)) > 0
    else :
      return False
    
  def itemType(self, itemId) :
    self._checkContains(itemId)
    return self._canvas.type(itemId)
        
  def items( self ):
    self._checkValid()
    return self.find_all()
    
 #--- Helper methods. 
  def _checkValid( self ):
    if self._win.isClosed() :
      raise GraphicsWinError()

  def _checkContains(self, itemId) :
    if self._win.isClosed() : raise GraphicsWinError()
    # modified by DFT
    # if itemId not in self : raise GraphicsObjError()
    return self.__contains__( itemId )


## This class defines a basic top level window that can contains a digital
#  image, the pixels of which can be accessed or set.
#
class ImageWindow :
  ## Creates a new image window with an empty image.
  #  @param width The horizontal size of the image in pixels.
  #  @param height The vertical size of the image in pixels.
  #        
  def __init__(self, width = 400, height = 400):    
    global TheMainWindow
    
     # Is the window open or closed. It must be open to use most operations.
    self._isClosed = False
    
     # If this is the first toplevel window, remember it as the main window. The
     # event loop is only terminated when the main window is closed.
    if TheMainWindow is None :
      TheMainWindow = self         
    
     # Create a top-level window for the graphics window.
    self._tkwin = tk.Toplevel(rootWin, width=width, height=height, 
                              borderwidth=0, padx=0, pady=0, bd=0)
    self._tkwin.protocol("WM_DELETE_WINDOW", self.close)
    self._tkwin.title("")
      
     # Create the photo image and tk canvas inside the window.
    self._tkimage = tk.PhotoImage(width=width, height=height)
    self._tkcanvas = tk.Canvas(self._tkwin, width=width, height=height, 
                               bg = "white", bd = 0)
    
     # Add the photo image object to the canvas.    
    self._tkcanvas.create_image(0, 0, anchor="nw", image=self._tkimage)
    self._tkcanvas.pack()
    
     # Bring the window to the front of all other windows and force an update.
    self._tkwin.lift()
    self._tkwin.resizable(0, 0)    
    self._tkwin.update_idletasks()
        
  ## Sets the title of the window. By default, the window has no title.
  #  @param title A text string to which the title of the window is set. To
  #          remove the title, use pass an empty string to the method.
  #
  def setTitle(self, title) :
    self._tkwin.title( title )
    
  ## Returns a Boolean indicating whether the window exists or was previously closed. 
  #  Window operations can not be performed on a closed window.
  #  @return @c True if the window is closed and @c False otherwise.
  #
  def isClosed(self) :
    return self._isClosed
    
  ## Hides or iconifies the top level window. 
  #  The window still exists and can be displayed again using the show method.
  #
  def hide(self) :
    if self._isClosed : raise GraphicsWinError()
    self._tkwin.withdraw()
    self._tkwin.update_idletasks()
      
  ## Shows or deiconifies a previously hidden top level window.
  #
  def show(self) :
    if self._isClosed : raise GraphicsWinError()
    self._tkwin.deiconify()
    self._tkwin.update_idletasks()
    
  ## Closes and destroys the window. A closed window can not be used.
  # 
  def close( self ):    
     # Set the closed flag to true so no other ops can be performed on
     # the window object.
    if self._isClosed : return
    self._isClosed = True
    
     # Destroy the window and force an update so it will close when  
     # used in IDLE or Wing IDE.
    self._tkwin.destroy()
    self._tkwin.update_idletasks()
    
     # Terminate the mainloop so the program will exit.
    self._tkwin.quit()
   
  ## Starts the event loop which handles various window events. This causes
  #  the sequential execution of the program to stop and wait for the user 
  #  to click the close button on the main window or to call the quit method
  #  on any window. This method should only be called on the main window.
  #  
  def wait( self ):
    if self._isClosed : raise GraphicsWinError()
    self._tkwin.mainloop()

  ## Sets a pixel to a given RGB color.
  #  @param row, col The pixel coordinates.
  #  @param red, green, blue The discrete RGB color components in the range [0..255].
  #
  def setPixel(self, row, col, red, green, blue) :
    self._tkimage.put("#%02x%02x%02x" % (red, green, blue), (col, row))
  
  ## Returns a 3-tuple containing the RGB color of a given pixel.
  #  @param row, col The pixel coordinates.
  #  @returns An RGB color as a 3-tuple.
  #
  def getPixel(self, row, col) :
    string = self._tkimage.get(col, row)
    parts = string.split()
    return (int(parts[0]), int(parts[1]), int(parts[2]))    
    
# --- Defines special graphics exceptions that are raised when an error
# --- occurs in a GraphicsWindow method.
class GraphicsError( Exception ) :
  def __init__( self, message ):
    super(GraphicsError, self).__init__( message )

class GraphicsObjError( GraphicsError ) :
  def __init__( self ):
    super(GraphicsObjectError, self).__init__( "Invalid object id." )

class GraphicsWinError( GraphicsError ) :
  def __init__( self ):
    super(GraphicsWinError, self).__init__(
              "Operation can not be performed on a closed window." )

class GraphicsParamError( GraphicsError ) :
  def __init__( self, message ):
    super(GraphicsParamError, self).__init__( message )

# --- Create an invisible root window and initialize the Tk system.
rootWin = tk.Tk()
rootWin.withdraw()

# --- Remember the first toplevel window created which serves as the main window.
TheMainWindow = None


#========================================================================
# ADDITIONS FOR CSC111
# (c) D. Thiebaut
#    _       _     _ _ _   _                ____ ____   ____ _   _   _ 
#   / \   __| | __| (_) |_(_) ___  _ __    / ___/ ___| / ___/ | / | / |
#  / _ \ / _` |/ _` | | __| |/ _ \| '_ \  | |   \___ \| |   | | | | | |
# / ___ \ (_| | (_| | | |_| | (_) | | | | | |___ ___) | |___| | | | | |
#/_/   \_\__,_|\__,_|_|\__|_|\___/|_| |_|  \____|____/ \____|_| |_| |_|
#
#========================================================================
# Circle: a class holding the coordinate of the center of a circle, its diameter,
#           and its color
class Circle:
    def __init__( self, x, y, diameter, color ):
        self._x       = x 
        self._y       = y 
        self._diameter= diameter
        self._color  = color   # list of 3 ints between 0 and 255
        self._id     = None

    def getId( self ):
        return self._id
      
    def draw( self, canvas ):
        xAnchor = self._x-self._diameter//2
        yAnchor = self._y-self._diameter//2
        canvas.setFill( self._color[0], self._color[1], self._color[2] )            
        self._id = canvas.drawOval( xAnchor,
                             yAnchor,
                             self._diameter,
                             self._diameter )

    def move( self, canvas, dx, dy ):
        canvas.shiftItem( self._id, dx, dy)
        
# Wheel: a class holding the coordinates of the center of two concentric circles
# and its outer diameter.  The color of the outside circle is black by default, and
# the color of the one inside is grey.
class Wheel:
    def __init__( self, x, y, diameter ):
        self._c1 = Circle( x, y, diameter, ( 0,0,0 ) )
        self._c2 = Circle( x, y, diameter/2, ( 200, 200, 200 ) )

    def draw( self, canvas ):
        self._c1.draw( canvas )
        self._c2.draw( canvas )

    def move( self, canvas, dx, dy ):
        self._c1.move( canvas, dx, dy)
        self._c2.move( canvas, dx, dy )

# Rectangle: Holds the coordinates of the top-left corner,
# the width and the height of the rectangle, as well as its
# color
class Rectangle:
    def __init__( self, x, y, width, height, color ):
        self._x      = x
        self._y      = y
        self._width  = width
        self._height = height
        self._color  = color   # list of 3 ints between 0 and 255
        self._id     = None

    def setRandomColor( self ):
        r = randrange( 256 )
        g = randrange( 256 )
        b = randrange( 256 )
        self._color = (r, g, b )

    def draw( self, canvas ):
        canvas.setFill( self._color[0], self._color[1], self._color[2] )            
        self._id = canvas.drawRect( self._x, self._y, self._width, self._height )

    def move( self, canvas, dx, dy ):
        canvas.shiftItem( self._id, dx, dy)

# Polygon: holds the list of coordinates of all the points forming
# a polygon, as well as its color.  Note that at least 3 points (6 coordinats)
# are required, otherwise the graphics library will raise an error.
class Polygon:
    def __init__( self, *args ):
        self._args   = args
        self._coords = args[ 0 ]
        self._color  = args[-1]
        self._id     = None

    def __str__( self ):
      s = "Poly = "
      for arg in self._args:
          s = s + str( arg ) + ", "
      return s
          
    def draw( self, canvas ):
      canvas.setFill( self._color[0], self._color[1], self._color[2] )
      self._id = canvas.drawPolygon( self._coords )

    def move( self, canvas, dx, dy ):
        canvas.shiftItem( self._id, dx, dy)

## Menu: a class that defines basic shapes that appear at the top left
# of the screen.  Currently a left arrow, right arrow, plus and minus
# signs are supported.
# Public methods:
#  draw( canvas ):  draws the basic shapes (black) on the canvas
#  buttonClicked(): returns which button was clicked, or None if none
#                   were clicked.  The strings returned are
#                   "LeftArrow", "RightArrow", "Minus", "Plus"
class Menu:
    def __init__( self ):
        # right arrow
        basicShape = (0,3, 5,3, 5,1, 9,5, 5,9, 5,7, 0,7)
        shape      = (x + (1-i%2)*15 for i,x in enumerate( basicShape ) )        
        self._ra   = Polygon( shape, (0, 0, 0 ) )

        # left arrow
        basicShape = (-0,3, -5,3, -5,1, -9,5, -5,9, -5,7, -0,7)
        shape      = (x + (1-i%2)*10 for i,x in enumerate( basicShape ) )
        self._la   = Polygon( shape, (0, 0, 0 ) )

        # Minus
        basicShape = ( 0, 3, 9, 4 )
        self._mi   = Rectangle( 0+30, 3, 9, 4, (0, 0, 0) )

        # Plus
        basicShape = ( 0, 3, 10, 4 )
        self._pl1  = Rectangle( 0+45, 3, 10, 4, (0, 0, 0) )
        basicShape = ( 3, 0, 4, 10 )
        self._pl2  = Rectangle( 3+45, 0, 4, 10, (0, 0, 0) )
        
    def draw( self, canvas ):
        self._ra.draw( canvas )    
        self._la.draw( canvas )    
        self._mi.draw( canvas )    
        self._pl1.draw( canvas )
        self._pl2.draw( canvas )

    # Gets the x and y of the last mouse click event, and 
    # returns the name of the button clicked, or None if none of
    # the buttons are clicked
    def buttonClicked( self, x, y ):
        if y > 11 or x > 55:
            return None
        if x < 19:
            return "LeftArrow"
        elif x < 24:
            return "RightArrow"
        elif x < 39:
            return "Minus"
        else:
            return "Plus"