#! /usr/bin/env python2

# SSGridView.py
#
# Demonstrates the creation of widgets as part of a Canvas object
# This is used to build a simple spreadsheet GUI 
#
# Hans Vangheluwe March 2002
# last modified: 04/12/02  --- by Jean-Sébastien Bolduc
#
# TODO:
#  accept negative integers and reals as input (not strings)
#  move overCell to selectedCell when Up, Down, Left, Right, Click
#  scrolling canvas: Page Up/Down, 
#                    follow selectedCell when Up, Down, Left, Right
#  put FSA in a class 
#  draw row and column numbers
#  

# Required modules
from Tkinter import *   # basic Tkinter
import tkFileDialog     # file selection 
#                                                  *** MODIFIED FOR ASSGT 4 ***
# Need classes CellCoordinate and CellData:
from CCoord import *
from CData import *

#                                                  *** MODIFIED FOR ASSGT 4 ***
# Custom exception:
class AbstractError(Exception):
  """Abstract class cannot be instanciated."""
  pass

#                                                  *** MODIFIED FOR ASSGT 4 ***
# Define class AbstractObserver:
class AbstractObserver:
  """
  Abstract observer class: class SpreadsheetGridView inherits from this.
  """
  def __init__(self):
    """ AbstractObserver constructor.
    """
    # Prevent this class from being instantiated (abstract):
    if self.__class__ == AbstractObserver:
       raise AbstractError, "cannot instantiate abstract class"
  
  def update(self, row, col, data):
    """ row:Integer, col:Integer, data:CellData ->
    
    Dummy implementation for update method.
    The method is overridden in subclass SpreadsheetGridView
    """
    pass


class CellCoord:
  """
  A coordinate class (unbounded)
  To enhance readability: 
  refer to coord.row and coord.col 
  rather than coord[0] and coord[1]
  """
  def __init__(self, row=0, col=0):
    self.row=row
    self.col=col

  # string representation
  def __repr__(self):
    return '(row: '+`self.row`+', column: '+`self.col`+')'


class DataElement:
  """
  Data found in cells:
   - str: the stringValue (for any dataType)
   - dataType: "Unknown", "Integer", "Real", "String" or "Formula"
     Note how the dataType can, by design NEVER be "Unknown": 
     data is only stored in a cell if its dataType was determined. 
     Illegal input for example is never entered in a DataElement 
     (see the FSA describing behaviour).
   - integerValue: the integer value when dataType == "Integer"
   - realValue: the real value when dataType == "Real"
   - stringValue and formulaValue don't exist (both are str)
     Distinction is made by means of dataType
   - textObject: a text object drawn on the canvas displaying str
   Note: Python would allow us to use a single attribute "value"
   in which any type of value could be stored. To allow easy transition
   to other implementation languages, this is not done here.
  """
  def __init__(self, stringValue=None):
    if  stringValue==None:
     self.str=""
    else:
     self.str=stringValue
    self.dataType = "Unknown"

  def setType(self, dataType="Unknown"):
    self.dataType = dataType

  def getType(self):
    return self.dataType

  def setStringValue(self, stringValue=None):
    self.str=stringValue

  def getStringValue(self):
    return self.str

  def setIntegerValue(self, integerValue=0):
    self.integerValue = integerValue

  def getIntegerValue(self):
    return self.integerValue 

  def setRealValue(self, realValue=0.0):
    self.realValue = realValue

  def getRealValue(self):
    return self.realValue

  def setTextObject(self, textObject=None):
    self.textObject=textObject

  def getTextObject(self):
    return self.textObject

  def __str__(self):
    return self.str


#                                                  *** MODIFIED FOR ASSGT 4 ***
# SpreadsheetGridView is now a concrete observer:
class SpreadsheetGridView(Frame, AbstractObserver):
  """
  The window containing the Grid View
  """
  
  #                                                *** MODIFIED FOR ASSGT 4 ***
  # The observer pattern needs the update method defined in the concrete
  # observer.
  def update(self, row, col, data):
    """ row:Integer, col:Integer, data:CellData ->
  
    Override update method from parent class AbstractObserver. Since we adopt a
    "push" strategy, the method is parameterized with the relevant information.
  
    Note that class CellCoord (assignment 3) is NOT the same as
    class CellCoordinate (assignment 1). This is redundant, but we can
    live with that (for now)...
    """
    if data == None:  # Updated because of a Backspace
      position=(row, col)
      if (self.data.has_key(position)):
        # delete the textobject from the drawarea Canvas
        self.drawarea.delete(self.data[position].getTextObject())
        # delete the entry from self.data, the DataSheet dictionary
        del self.data[position]
        self.calcMaxDim()
    else:  # Regular updating
      self.setData(coord=CellCoord(row, col), stringValue=data.getValue()[0])

    print "Update cell (%d, %d) in %s" % (row, col, self.title)
    # print self.data  # for debugging
  
  def __init__(self,
               master=None,
               title="Spreadsheet View",
               data=None,
               dataOffsetHor=0, dataOffsetVer=0,
               height=30, width=7,
               cellHeight=18, cellWidth=50,
               padHor=10, padVer=10):
               
    #                                              *** MODIFIED FOR ASSGT 4 ***
    # Observer attaches itself to the subject:
    if not( data.attach(self, dataOffsetHor, dataOffsetVer, height, width) ):
      raise Exception, "Houston, we have a problem"
    print "Attached %s to subject" % (title)

    # All code in this __init__ may be considered
    # as the default action taken when entering the
    # spreadsheet Statechart

    # Initialize superclass 
    Frame.__init__(self, master)
    
    # Initialize packer
    self.pack(fill=BOTH, expand=1)

    # Make arguments available to all methods 
    # Note: should really test for validity
    #       for example, height and width should be >= 1
    self.window = master    # the parent Tk window
    self.title = title      # the window's title
    self.height = height    # the number of grid rows
    self.width  = width     # the number of grid columns
    self.cellHeight = cellHeight # height in pixels of one cell
    self.cellWidth  = cellWidth  # width in pixels of one cell
    self.padHor  = padHor   # horizontal distance of grid from canvas border
    self.padVer  = padVer   # vertical distance of grid from canvas border
    #                                              *** MODIFIED FOR ASSGT 4 ***
    # attributes below now used: 
    self.subject  = data       # the observed data
    self.dataOffsetHor= dataOffsetHor # horizontal offset from data(0,0)
    self.dataOffsetVer= dataOffsetVer # vertical offset from data(0,0)
    

    # Behaviour: see Statechart design

    # globalState = "VIEWING" or "EDITING"
    # Determines how to react to events
    self.globalState = "VIEWING"
   
    # In the "EDITING" state, need the following sub-states:
    #  have recognized sequence of keys pressed upto now as
    #  "UNKNOWN", "INTEGER", "REAL", "STRING", or "FORMULA"
    self.editingState = "UNKNOWN"
    
    # In the "EDITING" state, need (to set) the following variables: 

    # Variable to hold String representation
    # Contructed in addition to Real or Integer values
    self.enteredString = ""

    # Variable to hold Integer value (if Integer)
    self.enteredInteger = 0

    # Variable to hold Real value (if Real)
    # Variable to hold scale factor to handle digits after decimal point
    self.enteredReal = 0.0
    self.realScale = 1
 
    # A dictionary indexed by (row, column) keys
    # containing DataElement entries
    # Thanks to the use of a dictionary, the extent of
    # the width x height can become arbitrarily large.
    # This is meaningful as the DataSheet "matrix" is often sparse.
    # (see assignment 1 for performance difference between list and dict)
    self.data={}

    # The initial "bounding box" (area filled with data)
    self.maxDim=CellCoord(0,0)
 
    # Handle window destruction 
    self.window.protocol("WM_DELETE_WINDOW", self.wmQuit) 

    # Set the window's title
    self.window.title(self.title)

    # Initialize the overCell object
    # which contains the coordinates of the cell
    # the mouse is currently over.
    # This could be any cell, but (0,0) seems most reasonable
    # We make sure this is a meaningful cell (within the
    # spreadsheet) by "snapping" it into the grid boundaries 
    # by means of spreadsheetBoundedCoord()
    self.overCell = self.spreadsheetBoundedCoord(CellCoord(0,0))

    # The Tk variable overCellString
    # is used to display the coordinates of the cell the mouse is 
    # currently over (modulo spreadsheet size)
    # It is updated in the callback routine for the mouse <Motion> event
    self.overCellString = StringVar()
    self.overCellString.set(`self.overCell`)

    # The Tk variable overCellData
    # is used to display the data in the cell the mouse is currently over
    # (modulo spreadsheet size)
    # It is updated in the callback routine for the mouse <Motion> event
    self.overCellData = StringVar()
    if self.getData(self.overCell) == None:
      self.overCellData.set("")
    else:
      self.overCellData.set(self.getData(self.overCell).getStringValue()) 

    # Initialize selectedCell to be equal to overCell
    # This could be some other cell,
    # but it seems reasonable to make this the same as overCell.
    # Note: selectedCell determines where typed in data will go.
    self.selectedCell=CellCoord(self.overCell.row, self.overCell.col)

    # Create all widgets in the Frame
    self.createWidgets()
 
    # For testing
    #self.setData(coord=CellCoord(1, 0), stringValue=str(10))
    #self.setData(coord=CellCoord(0, 0), stringValue=str(1000))
    #self.setData(coord=CellCoord(3, 3), stringValue="hello")
    #self.setData(coord=CellCoord(2, 5), stringValue=self.title)
    #self.setData(coord=CellCoord(1, 0), stringValue=str(999))
    #self.setData(coord=CellCoord(0, 0), stringValue=None)

  def getData(self, coord):
    """
    Get data from DataSheet at coord position
    If no entry at that position, return None
    """
    position=(coord.row, coord.col)
    if (self.data.has_key(position)):
      return self.data[position]
    else:
      return None

  def delData(self, coord):
    """
    The entry at coord position gets removed from 
    the DataSheet dictionary. The corresponding drawarea
    textObject also gets deleted.
    """
    position=(coord.row, coord.col)
    if (self.data.has_key(position)):
      # delete the textobject from the drawarea Canvas
      self.drawarea.delete(self.data[position].getTextObject())
      # delete the entry from self.data, the DataSheet dictionary
      del self.data[position]
      self.calcMaxDim()

    # Update the overCellData variable monitoring
    # the content of the cell the mouse is currently over
    if (self.overCell.row == coord.row) and \
      (self.overCell.col == coord.col):
      # Note: rather than the above test, 
      #  should define comparison 
      #  (__eq__ since Python 2.1, __cmp__ before)
      # for the CellCoord class 
      self.overCellData.set("")
      
    #                                              *** MODIFIED FOR ASSGT 4 ***
    # Modify the subject (delete corresponding entry):
       
    # We add 1 because the lowest indexes for a SpreadsheetData are 1,
    # whereas they are 0 for a SpreadsheetGridView...
    ccoord = CellCoordinate(coord.row+1, coord.col+1)
    del self.subject[ccoord]
    print "Deleted cell (%d, %d) in subject" %                                \
          (ccoord.getRow(), ccoord.getColumn())

  def setData(self, coord, stringValue=None):
    """
    Set string in DataSheet at coord position.
    If a data element was present at that position, it gets overwritten.
    The corresponding drawarea textObject gets updated.
    If the mouse is currently over the (coord) cell,
    the overCellData view also gets updated.
    """
    position=(coord.row, coord.col)
    if (stringValue == None):
     print "depricated, should be using delData()"
    else:
      self.maxDim.row=max(self.maxDim.row, coord.row)
      self.maxDim.col=max(self.maxDim.col, coord.col)
      if self.data.has_key(position):
        self.data[position].setStringValue(stringValue)
        self.drawarea.itemconfigure(\
         self.data[position].getTextObject(), text=stringValue)
      else:
        self.data[position]=DataElement()
        text = self.drawarea.create_text(\
               self.padHor + int((coord.col+1/2.0)*self.cellWidth),
               self.padVer + int((coord.row+1/2.0)*self.cellHeight),
               text=stringValue,
               anchor="c") # centered
               # Later, a cell should have other attributes such as 
          #  font, font size, alignment, colour, ...
        self.data[position].setStringValue(stringValue)
        self.data[position].setTextObject(text)

    # Update the overCellData variable monitoring
    # the content of the cell the mouse is currently over
    if (self.overCell.row == coord.row) and \
       (self.overCell.col == coord.col):
       # Note: rather than the above test, 
       #  should define comparison 
       #  (__eq__ since Python 2.1, __cmp__ before)
       # for the CellCoord class 
       if self.getData(self.overCell) == None:
         print "depricated, should be using delData()"
         self.overCellData.set("")
       else:
         self.overCellData.set(self.getData(self.overCell).getStringValue())

  def calcMaxDim(self):
    """
    Calculate and set the maximum row, col values
    (an instance of the CellCoord class)
    """
    rowMax=0
    colMax=0
    for coord in self.data.keys():
     if (coord[0] > rowMax): rowMax = coord[0]
     if (coord[1] > colMax): colMax = coord[1]
    self.maxDim.row=rowMax
    self.maxDim.col=colMax

  def getMaxDim(self):
    """
    Returns the maximum row and column values
    (an instance of the CellCoord class)
    """
    return self.maxDim

  def printData(self):
    """
    print all data in the datasheet to stdout
    """
    maxDim=self.getMaxDim()
    for row in range(maxDim.row+1):
      print 'row %3d:' % row,
      for column in range(maxDim.col+1):
        if self.getData(CellCoord(row,column)) == None:
          print '%8s' % '--',
        else:
          print '%8s' % self.getData(CellCoord(row,column)).getStringValue(),
      print

  def wmQuit(self):
    # we may wish to put cleanup actions here

    # don't forget to accept un-confirmed typed in data
    self.acceptData()
    
    print self.title + " was closed (by Window Manager)"
    
    #                                              *** MODIFIED FOR ASSGT 4 ***
    # Detach the observer from the subject:
    self.subject.detach(self)
    print "Detached %s from subject" % (self.title)
    self.window.destroy()

  def viewQuit(self):
    # we may wish to put cleanup actions here

    # don't forget to accept un-confirmed typed in data
    self.acceptData()

    print self.title + " was closed (Quit View button pressed)"
    
    #                                              *** MODIFIED FOR ASSGT 4 ***
    # Detach the observer from the subject:
    self.subject.detach(self)
    print "Detached %s from subject" % (self.title)
    self.window.destroy()

  def viewQuitError(self):
    print self.title + " was closed (Fatal Error)"
    self.window.destroy()

  # The window and its widgets
  #
  def createWidgets(self):

    # Quit button at the bottom
    self.quitButton = Button(master=self,
                             takefocus=0,
                             text='Quit View',
                             foreground='red',
                             command=self.viewQuit)
    self.quitButton.pack(side=BOTTOM, fill=X)

    # Frame to hold top labels and print button
    self.topFrame = Frame(master=self, background='white')
    self.topFrame.pack(side=TOP,fill=X)

    # Label displaying the content of the cell the mouse is
    # currently over.
    self.overCellContent = Label(master=self.topFrame,
                                 takefocus=0,
                                 textvariable=self.overCellData,
                                 width=20,
                                 relief="groove",
                                 background='white',
                                 foreground='blue')
    self.overCellContent.pack(side=RIGHT, padx=4)

    # Label displaying the cell the mouse is currently over
    # The Tk variable self.overCellString is used for this
    self.overCellCoord = Label(master=self.topFrame,
                               takefocus=0,
                               textvariable=self.overCellString,
                               width=20,
                               relief="groove",
                               background='white',
                               foreground='blue')
    self.overCellCoord.pack(side=RIGHT, padx=4)

    # Button to print this spreadsheet view to file
    self.printButton = Button(master=self.topFrame,
                              takefocus=0,
                              text="Print View",
                              command=self.printView)
    self.printButton.pack(fill=Y)

    # Canvas (default 15cm x 8cm), stretchable
    # Scrollable region calculated from 
    #   self.height, self.width
    #   self.cellHeight, self.cellWidth
    #   self.padHor, self.padVer
    scrollHor = self.width*self.cellWidth + self.padHor*2 
    scrollVer = self.height*self.cellHeight + self.padVer*2 
    self.drawarea = Canvas(master=self,
                           takefocus=1,
                           width="15c", height="8c",
                           background="white",
                           scrollregion=(0, 0, scrollHor, scrollVer))

    # frame for scrollX scrollbar as well as blank corner
    self.scrollXframe = Frame(self)

    # scrollbars for drawarea
    self.scrollX = Scrollbar(self.scrollXframe, takefocus=0, orient=HORIZONTAL)
    self.scrollY = Scrollbar(self, takefocus=0, orient=VERTICAL)

    # link canvas, scrollbars, and scrolling events
    self.drawarea['xscrollcommand'] = self.scrollX.set
    self.drawarea['yscrollcommand'] = self.scrollY.set
    self.scrollX['command'] = self.drawarea.xview
    self.scrollY['command'] = self.drawarea.yview

    # a blank square for aesthetic reasons
    self.blankSquare = Canvas(self.scrollXframe, bg='lightgrey',
                        width=int(self.scrollY['width'])+2,
         height=int(self.scrollX['width'])+2)

    # pack it all
    self.blankSquare.pack(side=RIGHT)
    self.scrollX.pack(side=LEFT, fill=X, expand=1)
    self.scrollXframe.pack(side=BOTTOM, fill=X)
    self.scrollY.pack(side=RIGHT, fill=Y)

    self.drawarea.pack(fill=BOTH, expand=1)

    # Make sure the Canvas has the focus
    # This is useful if there are other widgets around in the same
    # window such as Entry fields.
    self.drawarea.focus_set()

    # An array of Rectangles in the canvas
    # For efficiency reasons, vertical and horizontal lines
    # are drawn ((width + 1) + (height + 1)) in total
    # rather than width * height rectangle objects.
    # Cell content properties are kept in self.data
    for row in range(self.height+1):
      self.drawarea.create_line(self.padHor,
                   self.padVer+row*self.cellHeight,
                   self.padHor+self.width*self.cellWidth,
                   self.padVer+row*self.cellHeight,
                   fill="grey")
    for column in range(self.width+1):
      self.drawarea.create_line(self.padHor+column*self.cellWidth,
                   self.padVer,
                   self.padHor+column*self.cellWidth,
                   self.padVer+self.height*self.cellHeight,
                   fill="grey")

    # Draw a thick blue rectangle self.overCellFrame
    # around the cell the mouse is over,
    # only if within the spreadsheet boundaries of course
    # ("snapped" to spreadsheet boundary)
    # This overCellFrame object will later be moved to whichever cell
    # the mouse moves to (as a result of mouse <Motion>).
    if not ((0 <= self.overCell.row < self.height) and\
            (0 <= self.overCell.col < self.width)):
      print "self.overCell should always be within spreadsheet boundaries"
      print "correct initialization and updating should have taken care"
      print "of this. Maybe a method updating self.overCell without"
      print "using self.spreadsheetBoundedCoord() was created"
      self.viewQuitError()
    self.overCellFrame = self.drawarea.create_rectangle(\
      self.padHor+self.overCell.col*self.cellWidth,
      self.padVer+self.overCell.row*self.cellHeight,
      self.padHor+(self.overCell.col+1)*self.cellWidth,
      self.padVer+(self.overCell.row+1)*self.cellHeight,
      width=1, outline="blue")

    # Draw a thick black rectangle self.selectedCellFrame
    # around the selected cell, only if within the spreadsheet
    # boundaries of course ("snapped" to spreadsheet boundary).
    # This selectedCellFrame object will later be moved to the
    # a new cell the user selects (as a result of mouse <ButtonPress-1>).
    if not ((0 <= self.selectedCell.row < self.height) and\
            (0 <= self.selectedCell.col < self.width)):
      print "self.selectedCell should always be within spreadsheet boundaries"
      print "correct initialization and updating should have taken care"
      print "of this. Maybe a method updating self.selectedCell without"
      print "using self.spreadsheetBoundedCoord() was created"
      self.viewQuitError()
    self.selectedCellFrame = self.drawarea.create_rectangle(\
      self.padHor+self.selectedCell.col*self.cellWidth,
      self.padVer+self.selectedCell.row*self.cellHeight,
      self.padHor+(self.selectedCell.col+1)*self.cellWidth,
      self.padVer+(self.selectedCell.row+1)*self.cellHeight,
      width=2, outline="black")

    # Bind mouse events in drawarea to callback methods
    #
    self.drawarea.bind("<Motion>", self.onDrawareaMouseMove)
    self.drawarea.bind("<ButtonPress-1>", self.onDrawareaMouse1Click)
    self.drawarea.bind("<BackSpace>", self.onDrawareaBackSpace)
    self.drawarea.bind("<Left>", self.onDrawareaLeft)
    self.drawarea.bind("<Right>", self.onDrawareaRight)
    self.drawarea.bind("<Up>", self.onDrawareaUp)
    self.drawarea.bind("<Down>", self.onDrawareaDown)
    self.drawarea.bind("<Return>", self.onDrawareaDown)
    self.drawarea.bind("<KP_Enter>", self.onDrawareaDown)
    self.drawarea.bind("<KeyPress>", self.onDrawareaKeyPressed)
    #                                              *** MODIFIED FOR ASSGT 4 ***
    # Data that is being edited is validated when GridView looses focus.
    # Omitting this won't crash the application, but can lead to funny
    # behaviour as the same cell can be edited at the same time in two 
    # different views.
    self.drawarea.bind("<FocusOut>", self.onDrawareaLeave)

  def cellCoords(self, mouseX, mouseY):
    """
    Utility method to convert mouse position (event.x, event.y)
    into cell coordinates (row, col)
    Returns a CellCoord object
    This conversion takes scrolling into account and translates
    screen coordinates into canvas coordinates (canvasx(), canvasy())
    """
    cellRow =\
     (int(self.drawarea.canvasy(mouseY)) - self.padVer)/self.cellHeight
    cellCol =\
     (int(self.drawarea.canvasx(mouseX)) - self.padHor)/self.cellWidth
    return CellCoord(cellRow, cellCol)

  def spreadsheetBoundedCoord(self, cellCoord):
    """
    Returns coordinates bounded by the size of the spreadsheet.
    If a coordinate exceeds the boundary, the boundary value is used.
    """
    boundedCoord=CellCoord(cellCoord.row,cellCoord.col)
    if   (cellCoord.row < 0):            boundedCoord.row = 0
    elif (cellCoord.row >= self.height): boundedCoord.row = self.height-1
    if   (cellCoord.col < 0):            boundedCoord.col = 0
    elif (cellCoord.col >= self.width):  boundedCoord.col = self.width-1
    return boundedCoord

  def moveCellFrame(self, cellFrame, oldCellCoord, newCellCoord):
    """
    Move the cellFrame rectangle from oldCellCoord to newCellCoord
    UPDATE oldCellCoord (passed by reference as all mutable objects
    are really object references is Python)
    In practice, this updates self.selectedCell (the usual oldCellCoord) !
    """

    # move the cellFrame to the new selected cell area
    deltaX = (newCellCoord.col - oldCellCoord.col)\
             * self.cellWidth
    deltaY = (newCellCoord.row - oldCellCoord.row)\
             * self.cellHeight
    self.drawarea.move(cellFrame, deltaX, deltaY)

    # update the selected cell
    oldCellCoord.row = newCellCoord.row
    oldCellCoord.col = newCellCoord.col

  # The callbacks
  #

  #                                                *** MODIFIED FOR ASSGT 4 ***
  # callback for <FocusOut> event:
  def onDrawareaLeave(self, event):
    """
    Callback for the <FocusOut> (GridView looses focus) event
    Just accept any data that is being edited.
    """
    self.acceptData()  

  def onDrawareaMouseMove(self, event):
    """
    Callback for the mouse <Motion> event on the drawarea canvas
    This updates the self.overCell variable
    and moves the self.overCellFrame rectangle
    Both, only in case the mouse is over a different cell
    than it was before
    """
    # in any high-level state
    if (self.globalState == "VIEWING") or (self.globalState == "EDITING"):
      cellCoord = self.spreadsheetBoundedCoord(\
                       self.cellCoords(event.x, event.y))
      # Only if the mouse moves over a different cell
      if (cellCoord.row != self.overCell.row) or\
         (cellCoord.col != self.overCell.col):
         self.moveCellFrame(self.overCellFrame, self.overCell, cellCoord)
         # Update the overCellString variable monitoring
         # the cell the mouse is currently over
         self.overCellString.set(self.overCell)
         # Update the overCellData variable monitoring
         # the content of the cell the mouse is currently over
         if self.getData(self.overCell) == None:
           self.overCellData.set("")
         else:
           self.overCellData.set(self.getData(self.overCell).getStringValue())
    else:
      print "Error: globalState must be either VIEWING or EDITING"

  def onDrawareaMouse1Click(self, event):
    """
    Callback for the mouse <Mouse-1> event on the drawarea canvas
    This updates the self.selectedCell variable
    and moves the self.selectedCellFrame rectangle.
    """
    # Clicking (even in the previously selected cell)
    # is interpreted as the end of entering data in a cell
    # Note: acceptData() itself tests whether we are in EDITING mode
    self.acceptData()
 
    # translate to cell coordinates and "snap" to within the spreadsheet
    newSelectedCell = self.spreadsheetBoundedCoord(\
                           self.cellCoords(event.x, event.y))

    # For efficiency reasons, 
    # only if the selected cell is different from the
    # previously selected one: move the self.selectedCellFrame
    if (newSelectedCell.row != self.selectedCell.row) or\
       (newSelectedCell.col != self.selectedCell.col):
       self.moveCellFrame(self.selectedCellFrame,
                          self.selectedCell, newSelectedCell)

  def onDrawareaLeft(self, event):
    """
    Callback for the <Left> (keyboard left arrow pressed) event
    Change selectedCell to the horizontal-previous cell 
    (modulo spreadsheet size)
    """
    self.acceptData()
    newCol = self.selectedCell.col - 1
    newRow = self.selectedCell.row
    if (newCol < 0):
       newCol = self.width - 1
       newRow = (newRow - 1)
       if (newRow < 0):
         newRow = self.height - 1
    self.moveCellFrame(self.selectedCellFrame,
                       self.selectedCell, CellCoord(newRow, newCol))

  def onDrawareaRight(self, event):
    """
    Callback for the <Right> (keyboard right arrow pressed) event
    Change selectedCell to the horizontal-next cell 
    (modulo spreadsheet size)
    """
    self.acceptData()
    if (self.selectedCell.col < self.width-1):
       newRow = self.selectedCell.row
    else:
       newRow = (self.selectedCell.row + 1) % self.height
    newCol = (self.selectedCell.col + 1) % self.width
    self.moveCellFrame(self.selectedCellFrame,
                       self.selectedCell, CellCoord(newRow, newCol))

  def onDrawareaUp(self, event):
    """
    Callback for the <Up> (keyboard up arrow pressed) event
    Change selectedCell to the vertical-previous cell 
    (modulo spreadsheet size)
    """
    self.acceptData()
    newRow = self.selectedCell.row -1 
    newCol = self.selectedCell.col 
    if (newRow < 0):
       newRow = self.height - 1
       newCol = newCol - 1
       if (newCol < 0):
         newCol = self.width - 1
    self.moveCellFrame(self.selectedCellFrame,
                       self.selectedCell, CellCoord(newRow, newCol))

  def onDrawareaDown(self, event):
    """
    Callback for the <Down> (keyboard down arrow pressed) event
    Change selectedCell to the vertical-next cell 
    (modulo spreadsheet size)
    Note how this is also the callback for <Return> and <KP_Enter>
    (which implies that <Down>, <Return> and <KP_Enter> have
     exactly the same effect in this design, see also FSA)
    """
    self.acceptData()
    newCol = self.selectedCell.col
    if (self.selectedCell.row < self.height-1):
       newRow = self.selectedCell.row + 1
    else:
       newRow = 0
       newCol = newCol + 1
       if (newCol > self.width -1):
         newCol = 0
    self.moveCellFrame(self.selectedCellFrame,
                       self.selectedCell, CellCoord(newRow, newCol))

  def acceptData(self):
    """
    When in EDITING mode, take data recognized during editing
    and store in the selectedCell DataElement data cell.
    If only invalid keys were pressed, we will still be in 
    the UNKNOWN state. In this design, no set-ting will be done
    in this case.
    """
    if self.globalState == "EDITING":
      self.globalState = "VIEWING"
      if self.editingState == "UNKNOWN":
       print "Ignoring empty input"
      else: 
       self.setData(coord=self.selectedCell,
                    stringValue=self.enteredString)
       position = (self.selectedCell.row, self.selectedCell.col)
       if self.editingState == "INTEGER":
        self.data[position].setType("Integer") 
        self.data[position].setIntegerValue(self.enteredInteger) 
       elif self.editingState == "REAL":
        self.data[position].setType("Real") 
        self.data[position].setRealValue(self.enteredReal) 
       elif self.editingState == "STRING":
        self.data[position].setType("String") 
       elif self.editingState == "FORMULA":
        # a single '=' is not a formula but a string
        if self.enteredString == "=":
         self.data[position].setType("String") 
        else:
         self.data[position].setType("Formula") 
       else:
        print "Error: invalid editingState"
       print self.data[position].getType() 
       #                                           *** MODIFIED FOR ASSGT 4 ***
       # Modify the subject:
       
       # We add 1 because the lowest indexes for a SpreadsheetData are 1,
       # whereas they are 0 for a SpreadsheetGridView...
       ccoord = CellCoordinate(self.selectedCell.row+1,                       \
             self.selectedCell.col+1)
       if self.data[position].getType() == "Integer":
         numValue = self.data[position].getIntegerValue()
       elif  self.data[position].getType() == "Real":
         numValue = self.data[position].getRealValue()
       else:
         numValue = None
       self.subject[ccoord] = CellData(self.data[position].getStringValue(),  \
             self.data[position].getType(),                                   \
             numValue)
       print "Updated cell (%d, %d) in subject to %s" %                       \
             ( ccoord.getRow(), ccoord.getColumn(), str(self.subject[ccoord]) )
             
  # Helper methods for onDrawareaKeyPressed()
  # to determine the type of character typed in.
  # All is... methods return Boolean TRUE (1) or FALSE (0)

  def isAlphaChar(self, keysym):
   """
   <AlphaChar> in the range a - z, A - Z
   """
   return ( (len(keysym) == 1) and
            (('a' <= keysym <= 'z') or
             ('A' <= keysym <= 'Z')) )

  def isNonAlphaChar(self, keysym):
   """
   <NonAlphaChar> in { ~!@#$%^&*()-_+;:'",<>?[]}
   Notes: 
    - Future enhancement of the design:
    we could split up <NonAlphaChar> into those 
    allowed only in FORMULA mode and those allowed
    in STRING mode.
    - '/' is currently not in the list (for the sake of demonstration)
   """
   nonAlphaChars = ['asciitilde',    # ~
                    'exclam',        # !
                    'at',            # @
                    'numbersign',    # #
                    'dollar',        # $
                    'percent',       # %
                    'asciicircum',   # ^
                    'ampersand',     # &
                    'asterisk',      # *
                    'parenleft',     # (
                    'parenright',    # )
                    'minus',         # -
                    'underscore',    # _
                    'plus',          # +
                    'semicolon',     # ;
                    'colon',         # :
                    'apostrophe',    # '
                    'quotedbl',      # "
                    'comma',         # ,
                    'less',          # <
                    'greater',       # >
                    'question',      # ?
                    'bracketleft',   # [
                    'bracketright']  # ]
   return keysym in nonAlphaChars

  def isDigitChar(self, keysym):
   """
   <DigitChar> in the range 0 - 9 
   """
   return ('0' <= keysym <=  '9')
   # TODO: also recognize digits entered via 
   # the keypad 'KP_d'

  def isPeriod(self, keysym):
   """
   <period> = '.'
   """
   return (keysym == 'period')

  def isEquals(self, keysym):
   """
   <equals> = '=' 
   """
   return (keysym == 'equal')

  def onDrawareaKeyPressed(self, event):
    """
    Callback for the KeyPress event
    See the design Statechart !!!!
    """
    # Used the following to find out the keysyms
    # print 'key pressed', event.x, event.y 
    # print event.keysym, event.keysym_num, event.keycode

    # Default entry of the EDITING state from the VIEWING state
    if self.globalState == "VIEWING":
      self.globalState = "EDITING"
      self.editingState = "UNKNOWN"
      self.enteredString = ""
      self.enteredInteger = 0
      self.enteredReal = 0.0
      self.realScale = 1

    # no illegal input character encountered yet
    illegal = 0

    # Transition from the UNKNOWN state
    if self.editingState == "UNKNOWN":
      if self.isAlphaChar(event.keysym):
        self.editingState = "STRING"
        self.enteredString = self.enteredString + event.keysym
      elif self.isDigitChar(event.keysym):
        self.editingState = "INTEGER"
        self.enteredInteger = 0
        self.enteredInteger = self.enteredInteger*10 + int(event.keysym)
        self.enteredString = self.enteredString + event.keysym
      elif self.isPeriod(event.keysym):
        self.editingState = "REAL"
        self.enteredReal = 0.0 
        self.realScale = 0.1
        self.enteredString = self.enteredString + '.'
      elif self.isEquals(event.keysym):
        self.editingState = "FORMULA"
        self.enteredString = self.enteredString + '='
        # another option would be to not incorporate the '=' in the string
        # Caveat: a single '=' will be interpreted as a string
        # rather than a formula as is common in spreadsheets
      elif self.isNonAlphaChar(event.keysym):
        self.editingState = "STRING"
        self.enteredString = self.enteredString + chr(event.keysym_num) 
      # Ignore any other <Illegal> character
      # self.editingState remains UNKNOWN
      else:
        illegal = 1
        #print "illegal character <" + event.keysym + "> entered"

    # Transition from the INTEGER state
    elif self.editingState == "INTEGER":
      if self.isAlphaChar(event.keysym):
        self.editingState = "STRING"
        self.enteredString = self.enteredString + event.keysym
      elif self.isDigitChar(event.keysym):
        self.editingState = "INTEGER" # as it was before
        self.enteredInteger = self.enteredInteger*10 + int(event.keysym)
        self.enteredString = self.enteredString + event.keysym
      elif self.isPeriod(event.keysym):
        self.editingState = "REAL"
        self.enteredReal = float(self.enteredInteger) 
        self.realScale = 0.1
        self.enteredString = self.enteredString + '.'
      elif self.isEquals(event.keysym):
        self.editingState = "STRING"
        self.enteredString = self.enteredString + '='
      elif self.isNonAlphaChar(event.keysym):
        self.editingState = "STRING"
        self.enteredString = self.enteredString + chr(event.keysym_num) 
      # Ignore any other <Illegal> character
      # self.editingState remains INTEGER
      else:
        illegal = 1
        #print "illegal character <" + event.keysym + "> entered"

    # Transition from the REAL state
    elif self.editingState == "REAL":
      if self.isAlphaChar(event.keysym):
        self.editingState = "STRING"
        self.enteredString = self.enteredString + event.keysym
      elif self.isDigitChar(event.keysym):
        self.editingState = "REAL" # as it was before
        self.enteredReal = self.enteredReal +\
                           int(event.keysym)*self.realScale
        self.realScale = self.realScale/10
        self.enteredString = self.enteredString + event.keysym
      elif self.isPeriod(event.keysym):
        self.editingState = "STRING"
        self.enteredString = self.enteredString + '.'
      elif self.isEquals(event.keysym):
        self.editingState = "STRING"
        self.enteredString = self.enteredString + '='
      elif self.isNonAlphaChar(event.keysym):
        self.editingState = "STRING"
        self.enteredString = self.enteredString + chr(event.keysym_num) 
      # Ignore any other <Illegal> character
      # self.editingState remains REAL 
      else:
        illegal = 1
        #print "illegal character <" + event.keysym + "> entered"

    # Transition from the STRING or FORMULA states
    elif ((self.editingState == "STRING") or
          (self.editingState == "FORMULA")):
      # Note how self.editingState remains what it was before 
      if (\
          self.isAlphaChar(event.keysym) or
          self.isDigitChar(event.keysym) or
          self.isPeriod(event.keysym) or
          self.isEquals(event.keysym) or
          self.isNonAlphaChar(event.keysym)):
        self.enteredString = self.enteredString + chr(event.keysym_num)
      # Ignore any other <Illegal> character
      else:
        illegal = 1
        #print "illegal character <" + event.keysym + "> entered"

    else:
      print "Error: in unknown editingState " + self.editingState
      # Note: should really abort now as getting here
      # means there is a bug

      illegal = 1

    if not illegal: 
      # Update (after each entered valid key) the display
      # Only once the value is set will (in assignment 4)
      # the data be sent to the subject (from where 
      # all observers will be notified).
      self.setData(coord=self.selectedCell,
                   stringValue=self.enteredString)
    print "editingState = " + self.editingState

  def onDrawareaBackSpace(self, event):
    """
    process <BackSpace> to remove a cell entry 
    TODO: let <BackSpace> do cell editing, remove if empty string
          this will probably mean treating BackSpace as a KeyPress
    """
    if self.globalState == "EDITING":
      self.globalState = "VIEWING"
    # Remove the data
    # This will also update the overCellData view
    self.delData(coord=self.selectedCell)

  def printView(self):
    """
    Print the drawn-on area of the canvas to a PostScript file.
    Ask the user for filename interactively.
    """
    # For debugging, print non-empty data area to stdout
    self.printData()

    # Ask user for file to generate Postscipt in
    printFilename = \
     tkFileDialog.asksaveasfilename(filetypes=[("Postscript files", "*.ps")])

    # boundingbox of all objects on canvas
    bbox=self.drawarea.bbox(ALL)    

    # generate Postscript
    self.drawarea.postscript(file=printFilename,
                             width=bbox[2]+10, height=bbox[3]+10)
                             
# End of file SSGridView.py

